Esempio n. 1
0
class Patch(Command):
    """Applies a specific patch from a RB server.

    The patch file indicated by the request id is downloaded from the
    server and then applied locally."""
    name = "patch"
    author = "The Review Board Project"
    args = "<request-id>"
    option_list = [
        Option("--diff-revision",
               dest="diff_revision",
               default=None,
               help="revision id of diff to be used as patch"),
        Option("--px",
               dest="px",
               default=None,
               help="numerical pX argument for patch"),
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("-d",
               "--debug",
               action="store_true",
               dest="debug",
               config_key="DEBUG",
               default=False,
               help="display debug output"),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
    ]

    def get_patch(self, request_id, diff_revision=None):
        """Given a review request ID and a diff revision,
        return the diff as a string, the used diff revision,
        and its basedir.

        If a diff revision is not specified, then this will
        look at the most recent diff.
        """
        try:
            diffs = self.root_resource \
                .get_review_requests() \
                .get_item(request_id) \
                .get_diffs()
        except APIError, e:
            die("Error getting diffs: %s" % (e))

        # Use the latest diff if a diff revision was not given.
        # Since diff revisions start a 1, increment by one, and
        # never skip a number, the latest diff revisions number
        # should be equal to the number of diffs.
        if diff_revision is None:
            diff_revision = diffs.total_results

        try:
            diff = diffs.get_item(diff_revision)
            diff_body = diff.get_patch().data
            base_dir = diff.basedir
        except APIError:
            die('The specified diff revision does not exist.')

        return diff_body, diff_revision, base_dir
Esempio n. 2
0
class Post(Command):
    """Create and update review requests."""
    name = 'post'
    author = 'The Review Board Project'
    description = 'Uploads diffs to create and update review requests.'
    args = '[revisions]'

    GUESS_AUTO = 'auto'
    GUESS_YES = 'yes'
    GUESS_NO = 'no'
    GUESS_YES_INPUT_VALUES = (True, 'yes', 1, '1')
    GUESS_NO_INPUT_VALUES = (False, 'no', 0, '0')
    GUESS_CHOICES = (GUESS_AUTO, GUESS_YES, GUESS_NO)

    option_list = [
        OptionGroup(
            name='Posting Options',
            description='Controls the behavior of a post, including what '
            'review request gets posted and how, and what '
            'happens after it is posted.',
            option_list=[
                Option('-u',
                       '--update',
                       dest='update',
                       action='store_true',
                       default=False,
                       help='Automatically determines the existing review '
                       'request to update.',
                       added_in='0.5.3'),
                Option('-r',
                       '--review-request-id',
                       dest='rid',
                       metavar='ID',
                       default=None,
                       help='Specifies the existing review request ID to '
                       'update.'),
                Option('-p',
                       '--publish',
                       dest='publish',
                       action='store_true',
                       default=False,
                       config_key='PUBLISH',
                       help='Publishes the review request immediately after '
                       'posting.'
                       '\n'
                       'All required fields must already be filled in '
                       'on the review request or must be provided when '
                       'posting.'),
                Option('-o',
                       '--open',
                       dest='open_browser',
                       action='store_true',
                       config_key='OPEN_BROWSER',
                       default=False,
                       help='Opens a web browser to the review request '
                       'after posting.'),
                Option('--submit-as',
                       dest='submit_as',
                       metavar='USERNAME',
                       config_key='SUBMIT_AS',
                       default=None,
                       help='The username to use as the author of the '
                       'review request, instead of the logged in user.',
                       extended_help=(
                           "This is useful when used in a repository's "
                           "post-commit script to update or create review "
                           "requests. See :ref:`automating-rbt-post` for "
                           "more information on this use case.")),
                Option('--change-only',
                       dest='change_only',
                       action='store_true',
                       default=False,
                       help='Updates fields from the change description, '
                       'but does not upload a new diff '
                       '(Perforce/Plastic only).'),
                Option('--diff-only',
                       dest='diff_only',
                       action='store_true',
                       default=False,
                       help='Uploads a new diff, but does not update '
                       'fields from the change description '
                       '(Perforce/Plastic only).'),
            ]),
        Command.server_options,
        Command.repository_options,
        OptionGroup(
            name='Review Request Field Options',
            description='Options for setting the contents of fields in the '
            'review request.',
            option_list=[
                Option('-g',
                       '--guess-fields',
                       dest='guess_fields',
                       action='store',
                       config_key='GUESS_FIELDS',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Equivalent to setting both --guess-summary '
                       'and --guess-description.',
                       extended_help=(
                           'This can optionally take a value to control the '
                           'guessing behavior. See :ref:`guessing-behavior` '
                           'for more information.')),
                Option('--guess-summary',
                       dest='guess_summary',
                       action='store',
                       config_key='GUESS_SUMMARY',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Generates the Summary field based on the '
                       'commit messages (Bazaar/Git/Mercurial only).',
                       extended_help=(
                           'This can optionally take a value to control the '
                           'guessing behavior. See :ref:`guessing-behavior` '
                           'for more information.')),
                Option('--guess-description',
                       dest='guess_description',
                       action='store',
                       config_key='GUESS_DESCRIPTION',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Generates the Description field based on the '
                       'commit messages (Bazaar/Git/Mercurial only).',
                       extended_help=(
                           'This can optionally take a value to control the '
                           'guessing behavior. See :ref:`guessing-behavior` '
                           'for more information.')),
                Option('--change-description',
                       default=None,
                       metavar='TEXT',
                       help='A description of what changed in this update '
                       'of the review request. This is ignored for new '
                       'review requests.'),
                Option('--summary',
                       dest='summary',
                       metavar='TEXT',
                       default=None,
                       help='The new contents for the Summary field.'),
                Option('--description',
                       dest='description',
                       metavar='TEXT',
                       default=None,
                       help='The new contents for the Description field.'),
                Option('--description-file',
                       dest='description_file',
                       default=None,
                       metavar='FILENAME',
                       help='A text file containing the new contents for the '
                       'Description field.'),
                Option('--testing-done',
                       dest='testing_done',
                       metavar='TEXT',
                       default=None,
                       help='The new contents for the Testing Done field.'),
                Option('--testing-done-file',
                       dest='testing_file',
                       default=None,
                       metavar='FILENAME',
                       help='A text file containing the new contents for the '
                       'Testing Done field.'),
                Option('--branch',
                       dest='branch',
                       config_key='BRANCH',
                       metavar='BRANCH',
                       default=None,
                       help='The branch the change will be committed on or '
                       'affects. This is a free-form field and does not '
                       'control any behavior.'),
                Option('--bugs-closed',
                       dest='bugs_closed',
                       metavar='BUG_ID[,...]',
                       default=None,
                       help='The comma-separated list of bug IDs closed.'),
                Option('--target-groups',
                       dest='target_groups',
                       config_key='TARGET_GROUPS',
                       metavar='NAME[,...]',
                       default=None,
                       help='The names of the groups that should perform the '
                       'review.'),
                Option('--target-people',
                       dest='target_people',
                       metavar='USERNAME[,...]',
                       config_key='TARGET_PEOPLE',
                       default=None,
                       help='The usernames of the people who should perform '
                       'the review.'),
                Option('--depends-on',
                       dest='depends_on',
                       config_key='DEPENDS_ON',
                       metavar='ID[,...]',
                       default=None,
                       help='A comma-separated list of review request IDs '
                       'that this review request will depend on.',
                       added_in='0.6.1'),
                Option('--markdown',
                       dest='markdown',
                       action='store_true',
                       config_key='MARKDOWN',
                       default=False,
                       help='Specifies if the summary and description should '
                       'be interpreted as Markdown-formatted text.'
                       '\n'
                       'This is only supported in Review Board 2.0+.',
                       added_in='0.6'),
            ]),
        Command.diff_options,
        Command.perforce_options,
        Command.subversion_options,
        Command.tfs_options,
    ]

    def post_process_options(self):
        # -g implies --guess-summary and --guess-description
        if self.options.guess_fields:
            self.options.guess_fields = self.normalize_guess_value(
                self.options.guess_fields, '--guess-fields')

            self.options.guess_summary = self.options.guess_fields
            self.options.guess_description = self.options.guess_fields

        if self.options.revision_range:
            raise CommandError(
                'The --revision-range argument has been removed. To post a '
                'diff for one or more specific revisions, pass those '
                'revisions as arguments. For more information, see the '
                'RBTools 0.6 Release Notes.')

        if self.options.svn_changelist:
            raise CommandError(
                'The --svn-changelist argument has been removed. To use a '
                'Subversion changelist, pass the changelist name as an '
                'additional argument after the command.')

        # Only one of --description and --description-file can be used
        if self.options.description and self.options.description_file:
            raise CommandError('The --description and --description-file '
                               'options are mutually exclusive.\n')

        # If --description-file is used, read that file
        if self.options.description_file:
            if os.path.exists(self.options.description_file):
                with open(self.options.description_file, 'r') as fp:
                    self.options.description = fp.read()
            else:
                raise CommandError(
                    'The description file %s does not exist.\n' %
                    self.options.description_file)

        # Only one of --testing-done and --testing-done-file can be used
        if self.options.testing_done and self.options.testing_file:
            raise CommandError('The --testing-done and --testing-done-file '
                               'options are mutually exclusive.\n')

        # If --testing-done-file is used, read that file
        if self.options.testing_file:
            if os.path.exists(self.options.testing_file):
                with open(self.options.testing_file, 'r') as fp:
                    self.options.testing_done = fp.read()
            else:
                raise CommandError('The testing file %s does not exist.\n' %
                                   self.options.testing_file)

        # If we have an explicitly specified summary, override
        # --guess-summary
        if self.options.summary:
            self.options.guess_summary = self.GUESS_NO
        else:
            self.options.guess_summary = self.normalize_guess_value(
                self.options.guess_summary, '--guess-summary')

        # If we have an explicitly specified description, override
        # --guess-description
        if self.options.description:
            self.options.guess_description = self.GUESS_NO
        else:
            self.options.guess_description = self.normalize_guess_value(
                self.options.guess_description, '--guess-description')

        # If we have an explicitly specified review request ID, override
        # --update
        if self.options.rid and self.options.update:
            self.options.update = False

    def normalize_guess_value(self, guess, arg_name):
        if guess in self.GUESS_YES_INPUT_VALUES:
            return self.GUESS_YES
        elif guess in self.GUESS_NO_INPUT_VALUES:
            return self.GUESS_NO
        elif guess == self.GUESS_AUTO:
            return guess
        else:
            raise CommandError('Invalid value "%s" for argument "%s"' %
                               (guess, arg_name))

    def get_repository_path(self, repository_info, api_root):
        """Get the repository path from the server.

        This will compare the paths returned by the SCM client
        with those one the server, and return the first match.
        """
        if isinstance(repository_info.path, list):
            repositories = api_root.get_repositories(only_fields='path',
                                                     only_links='')

            try:
                while True:
                    for repo in repositories:
                        if repo['path'] in repository_info.path:
                            repository_info.path = repo['path']
                            raise StopIteration()

                    repositories = repositories.get_next()
            except StopIteration:
                pass

        if isinstance(repository_info.path, list):
            error_str = [
                'There was an error creating this review request.\n',
                '\n',
                'There was no matching repository path found on the server.\n',
                'Unknown repository paths found:\n',
            ]

            for foundpath in repository_info.path:
                error_str.append('\t%s\n' % foundpath)

            error_str += [
                'Ask the administrator to add one of these repositories\n',
                'to the Review Board server.\n',
            ]

            raise CommandError(''.join(error_str))

        return repository_info.path

    def post_request(self,
                     repository_info,
                     repository,
                     server_url,
                     api_root,
                     review_request_id=None,
                     changenum=None,
                     diff_content=None,
                     parent_diff_content=None,
                     commit_id=None,
                     base_commit_id=None,
                     submit_as=None,
                     retries=3,
                     base_dir=None):
        """Creates or updates a review request, and uploads a diff.

        On success the review request id and url are returned.
        """
        supports_posting_commit_ids = \
            self.tool.capabilities.has_capability('review_requests',
                                                  'commit_ids')

        if review_request_id:
            review_request = get_review_request(
                review_request_id,
                api_root,
                only_fields='absolute_url,bugs_closed,id,status',
                only_links='diffs,draft')

            if review_request.status == 'submitted':
                raise CommandError(
                    'Review request %s is marked as %s. In order to update '
                    'it, please reopen the review request and try again.' %
                    (review_request_id, review_request.status))
        else:
            # No review_request_id, so we will create a new review request.
            try:
                request_data = {'repository': repository}

                if changenum:
                    request_data['changenum'] = changenum
                elif commit_id and supports_posting_commit_ids:
                    request_data['commit_id'] = commit_id

                if submit_as:
                    request_data['submit_as'] = submit_as

                review_requests = api_root.get_review_requests(
                    only_fields='', only_links='create')
                review_request = review_requests.create(**request_data)
            except APIError as e:
                if e.error_code == 204 and changenum:  # Change number in use.
                    rid = e.rsp['review_request']['id']
                    review_request = api_root.get_review_request(
                        review_request_id=rid,
                        only_fields='absolute_url,bugs_closed,id,status',
                        only_links='diffs,draft')

                    if not self.options.diff_only:
                        review_request = review_request.update(
                            changenum=changenum)
                else:
                    raise CommandError('Error creating review request: %s' % e)

        if (not repository_info.supports_changesets
                or not self.options.change_only):
            try:
                diff_kwargs = {
                    'parent_diff': parent_diff_content,
                    'base_dir': base_dir,
                }

                if (base_commit_id and self.tool.capabilities.has_capability(
                        'diffs', 'base_commit_ids')):
                    # Both the Review Board server and SCMClient support
                    # base commit IDs, so pass that along when creating
                    # the diff.
                    diff_kwargs['base_commit_id'] = base_commit_id

                review_request.get_diffs(only_fields='').upload_diff(
                    diff_content, **diff_kwargs)
            except APIError as e:
                error_msg = [
                    u'Error uploading diff\n\n',
                ]

                if e.error_code == 101 and e.http_status == 403:
                    error_msg.append(u'You do not have permissions to modify '
                                     u'this review request\n')
                elif e.error_code == 219:
                    error_msg.append(
                        u'The generated diff file was empty. This '
                        u'usually means no files were\n'
                        u'modified in this change.\n')
                else:
                    error_msg.append(str(e).decode('utf-8') + u'\n')

                error_msg.append(
                    u'Your review request still exists, but the diff is '
                    u'not attached.\n')

                error_msg.append(u'%s\n' % review_request.absolute_url)

                raise CommandError(u'\n'.join(error_msg))

        try:
            draft = review_request.get_draft(only_fields='commit_id')
        except APIError as e:
            raise CommandError('Error retrieving review request draft: %s' % e)

        # Update the review request draft fields based on options set
        # by the user, or configuration.
        update_fields = {}

        if self.options.target_groups:
            update_fields['target_groups'] = self.options.target_groups

        if self.options.target_people:
            update_fields['target_people'] = self.options.target_people

        if self.options.depends_on:
            update_fields['depends_on'] = self.options.depends_on

        if self.options.summary:
            update_fields['summary'] = self.options.summary

        if self.options.branch:
            update_fields['branch'] = self.options.branch

        if self.options.bugs_closed:
            # Append to the existing list of bugs.
            self.options.bugs_closed = self.options.bugs_closed.strip(', ')
            bug_set = (set(re.split('[, ]+', self.options.bugs_closed))
                       | set(review_request.bugs_closed))
            self.options.bugs_closed = ','.join(bug_set)
            update_fields['bugs_closed'] = self.options.bugs_closed

        if self.options.description:
            update_fields['description'] = self.options.description

        if self.options.testing_done:
            update_fields['testing_done'] = self.options.testing_done

        if ((self.options.description or self.options.testing_done)
                and self.options.markdown
                and self.tool.capabilities.has_capability('text', 'markdown')):
            # The user specified that their Description/Testing Done are
            # valid Markdown, so tell the server so it won't escape the text.
            update_fields['text_type'] = 'markdown'

        if self.options.change_description:
            update_fields['changedescription'] = \
                self.options.change_description

        if self.options.publish:
            update_fields['public'] = True

        if supports_posting_commit_ids and commit_id != draft.commit_id:
            update_fields['commit_id'] = commit_id or ''

        if update_fields:
            try:
                draft = draft.update(**update_fields)
            except APIError as e:
                raise CommandError(u'\n'.join([
                    u'Error updating review request draft: %s\n' % e,
                    u'Your review request still exists, but the diff is '
                    u'not attached.\n',
                    u'%s\n' % review_request.absolute_url,
                ]))

        return review_request.id, review_request.absolute_url

    def check_guess_fields(self):
        """Checks and handles field guesses for the review request.

        This will attempt to guess the values for the summary and
        description fields, based on the contents of the commit message
        at the provided revisions, if requested by the caller.

        If the backend doesn't support guessing, or if guessing isn't
        requested, or if explicit values were set in the options, nothing
        will be set for the fields.
        """
        is_new_review_request = (not self.options.rid
                                 and not self.options.update)

        guess_summary = (self.options.guess_summary == self.GUESS_YES
                         or (self.options.guess_summary == self.GUESS_AUTO
                             and is_new_review_request))
        guess_description = (self.options.guess_description == self.GUESS_YES
                             or
                             (self.options.guess_description == self.GUESS_AUTO
                              and is_new_review_request))

        if guess_summary or guess_description:
            try:
                assert self.revisions
                commit_message = self.tool.get_commit_message(self.revisions)

                if commit_message:
                    if guess_summary:
                        self.options.summary = commit_message['summary']

                    if guess_description:
                        self.options.description = \
                            commit_message['description']
            except NotImplementedError:
                # The SCMClient doesn't support getting commit messages,
                # so we can't provide the guessed versions.
                pass

    def _ask_review_request_match(self, review_request):
        question = ("Update Review Request #%s: '%s'? " %
                    (review_request.id,
                     get_draft_or_current_value('summary', review_request)))

        return confirm(question)

    def main(self, *args):
        """Create and update review requests."""
        # The 'args' tuple must be made into a list for some of the
        # SCM Clients code. The way arguments were structured in
        # post-review meant this was a list, and certain parts of
        # the code base try and concatenate args to the end of
        # other lists. Until the client code is restructured and
        # cleaned up we will satisfy the assumption here.
        self.cmd_args = list(args)

        self.post_process_options()
        origcwd = os.path.abspath(os.getcwd())
        repository_info, self.tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(self.tool, api_root=api_root)

        if (self.options.exclude_patterns
                and not self.tool.supports_diff_exclude_patterns):

            raise CommandError(
                'The %s backend does not support excluding files via the '
                '-X/--exclude commandline options or the EXCLUDE_PATTERNS '
                '.reviewboardrc option.' % self.tool.name)

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        if self.options.diff_filename:
            self.revisions = None
            parent_diff = None
            base_commit_id = None
            commit_id = None

            if self.options.diff_filename == '-':
                if hasattr(sys.stdin, 'buffer'):
                    # Make sure we get bytes on Python 3.x
                    diff = sys.stdin.buffer.read()
                else:
                    diff = sys.stdin.read()
            else:
                try:
                    diff_path = os.path.join(origcwd,
                                             self.options.diff_filename)
                    with open(diff_path, 'rb') as fp:
                        diff = fp.read()
                except IOError as e:
                    raise CommandError('Unable to open diff filename: %s' % e)
        else:
            self.revisions = get_revisions(self.tool, self.cmd_args)

            if self.revisions:
                extra_args = None
            else:
                extra_args = self.cmd_args

            # Generate a diff against the revisions or arguments, filtering
            # by the requested files if provided.
            diff_info = self.tool.diff(
                revisions=self.revisions,
                include_files=self.options.include_files or [],
                exclude_patterns=self.options.exclude_patterns or [],
                extra_args=extra_args)

            diff = diff_info['diff']
            parent_diff = diff_info.get('parent_diff')
            base_commit_id = diff_info.get('base_commit_id')
            commit_id = diff_info.get('commit_id')

        repository = (self.options.repository_name
                      or self.options.repository_url
                      or self.get_repository_path(repository_info, api_root))

        base_dir = self.options.basedir or repository_info.base_path

        if len(diff) == 0:
            raise CommandError("There don't seem to be any diffs!")

        # Validate the diffs to ensure that they can be parsed and that
        # all referenced files can be found.
        #
        # Review Board 2.0.14+ (with the diffs.validation.base_commit_ids
        # capability) is required to successfully validate against hosting
        # services that need a base_commit_id. This is basically due to
        # the limitations of a couple Git-specific hosting services
        # (Beanstalk, Bitbucket, and Unfuddle).
        #
        # In order to validate, we need to either not be dealing with a
        # base commit ID (--diff-filename), or be on a new enough version
        # of Review Board, or be using a non-Git repository.
        can_validate_base_commit_ids = \
            self.tool.capabilities.has_capability('diffs', 'validation',
                                                  'base_commit_ids')

        if (not base_commit_id or can_validate_base_commit_ids
                or self.tool.name != 'Git'):
            # We can safely validate this diff before posting it, but we
            # need to ensure we only pass base_commit_id if the capability
            # is set.
            validate_kwargs = {}

            if can_validate_base_commit_ids:
                validate_kwargs['base_commit_id'] = base_commit_id

            try:
                diff_validator = api_root.get_diff_validation()
                diff_validator.validate_diff(repository,
                                             diff,
                                             parent_diff=parent_diff,
                                             base_dir=base_dir,
                                             **validate_kwargs)
            except APIError as e:
                msg_prefix = ''

                if e.error_code == 207:
                    msg_prefix = '%s: ' % e.rsp['file']

                raise CommandError('Error validating diff\n\n%s%s' %
                                   (msg_prefix, e))
            except AttributeError:
                # The server doesn't have a diff validation resource. Post as
                # normal.
                pass

        if repository_info.supports_changesets and 'changenum' in diff_info:
            changenum = diff_info['changenum']
            commit_id = changenum
        else:
            changenum = None

        if not self.options.diff_filename:
            # If the user has requested to guess the summary or description,
            # get the commit message and override the summary and description
            # options.
            self.check_guess_fields()

        if self.options.update and self.revisions:
            self.options.rid = guess_existing_review_request_id(
                repository_info,
                self.options.repository_name,
                api_root,
                api_client,
                self.tool,
                self.revisions,
                guess_summary=False,
                guess_description=False,
                is_fuzzy_match_func=self._ask_review_request_match)

            if not self.options.rid:
                raise CommandError('Could not determine the existing review '
                                   'request to update.')

        # If only certain files within a commit are being submitted for review,
        # do not include the commit id. This prevents conflicts if mutliple
        # files from the same commit are posted for review separately.
        if self.options.include_files:
            commit_id = None

        request_id, review_url = self.post_request(
            repository_info,
            repository,
            server_url,
            api_root,
            self.options.rid,
            changenum=changenum,
            diff_content=diff,
            parent_diff_content=parent_diff,
            commit_id=commit_id,
            base_commit_id=base_commit_id,
            submit_as=self.options.submit_as,
            base_dir=base_dir)

        diff_review_url = review_url + 'diff/'

        print('Review request #%s posted.' % request_id)
        print()
        print(review_url)
        print(diff_review_url)

        # Load the review up in the browser if requested to.
        if self.options.open_browser:
            try:
                import webbrowser
                if 'open_new_tab' in dir(webbrowser):
                    # open_new_tab is only in python 2.5+
                    webbrowser.open_new_tab(review_url)
                elif 'open_new' in dir(webbrowser):
                    webbrowser.open_new(review_url)
                else:
                    os.system('start %s' % review_url)
            except:
                logging.error('Error opening review URL: %s' % review_url)
Esempio n. 3
0
class Land(Command):
    """Land changes from a review request onto the remote repository.

    This command takes a review request, applies it to a feature branch,
    merges it with the specified destination branch, and pushes the
    changes to an upstream repository.

    Notes:
        The review request needs to be approved first.

        ``--local`` option can be used to skip the patching step.
    """

    name = 'land'
    author = 'The Review Board Project'

    needs_api = True
    needs_scm_client = True
    needs_repository = True

    args = '[<branch-name>]'
    option_list = [
        Option('--dest',
               dest='destination_branch',
               default=None,
               config_key='LAND_DEST_BRANCH',
               help='Specifies the destination branch to land changes on.'),
        Option('-r',
               '--review-request-id',
               dest='rid',
               metavar='ID',
               default=None,
               help='Specifies the review request ID.'),
        Option('--local',
               dest='is_local',
               action='store_true',
               default=None,
               help='Forces the change to be merged without patching, if '
               'merging a local branch. Defaults to true unless '
               '--review-request-id is used.'),
        Option('-p',
               '--push',
               dest='push',
               action='store_true',
               default=False,
               config_key='LAND_PUSH',
               help='Pushes the branch after landing the change.'),
        Option('-n',
               '--no-push',
               dest='push',
               action='store_false',
               default=False,
               config_key='LAND_PUSH',
               help='Prevents pushing the branch after landing the change, '
               'if pushing is enabled by default.'),
        Option('--squash',
               dest='squash',
               action='store_true',
               default=False,
               config_key='LAND_SQUASH',
               help='Squashes history into a single commit.'),
        Option('--no-squash',
               dest='squash',
               action='store_false',
               default=False,
               config_key='LAND_SQUASH',
               help='Disables squashing history into a single commit, '
               'choosing instead to merge the branch, if squashing is '
               'enabled by default.'),
        Option('-e',
               '--edit',
               dest='edit',
               action='store_true',
               default=False,
               help='Invokes the editor to edit the commit message before '
               'landing the change.'),
        Option('--delete-branch',
               dest='delete_branch',
               action='store_true',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Deletes the local branch after it's landed. Only used if "
               "landing a local branch. This is the default."),
        Option('--no-delete-branch',
               dest='delete_branch',
               action='store_false',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Prevents the local branch from being deleted after it's "
               "landed."),
        Option('--dry-run',
               dest='dry_run',
               action='store_true',
               default=False,
               help='Simulates the landing of a change, without actually '
               'making any changes to the tree.'),
        Option('--recursive',
               dest='recursive',
               action='store_true',
               default=False,
               help='Recursively fetch patches for review requests that the '
               'specified review request depends on. This is equivalent '
               'to calling "rbt patch" for each of those review '
               'requests.',
               added_in='1.0'),
        Command.server_options,
        Command.repository_options,
        Command.branch_options,
    ]

    def patch(self, review_request_id, squash=False):
        """Patch a single review request's diff using rbt patch.

        Args:
            review_request_id (int):
                The ID of the review request to patch.

            squash (bool, optional):
                Whether to squash multiple commits into a single commit.

        Raises:
            rbtools.commands.CommandError:
                There was an error applying the patch.
        """
        patch_command = [RB_MAIN, 'patch']
        patch_command.extend(build_rbtools_cmd_argv(self.options))

        if self.options.edit:
            patch_command.append('-c')
        else:
            patch_command.append('-C')

        if squash:
            patch_command.append('--squash')

        patch_command.append(six.text_type(review_request_id))

        rc, output = execute(patch_command,
                             ignore_errors=True,
                             return_error_code=True)

        if rc:
            raise CommandError('Failed to execute "rbt patch":\n%s' % output)

    def can_land(self, review_request):
        """Determine if the review request is land-able.

        A review request can be landed if it is approved or, if the Review
        Board server does not keep track of approval, if the review request
        has a ship-it count.

        This function returns the error with landing the review request or None
        if it can be landed.
        """
        try:
            is_rr_approved = review_request.approved
            approval_failure = review_request.approval_failure
        except AttributeError:
            # The Review Board server is an old version (pre-2.0) that
            # doesn't support the `approved` field. Determine it manually.
            if review_request.ship_it_count == 0:
                is_rr_approved = False
                approval_failure = \
                    'The review request has not been marked "Ship It!"'
            else:
                is_rr_approved = True
        except Exception as e:
            logging.exception(
                'Unexpected error while looking up review request '
                'approval state: %s', e)

            return ('An error was encountered while executing the land '
                    'command.')
        finally:
            if not is_rr_approved:
                return approval_failure

        return None

    def land(self,
             destination_branch,
             review_request,
             source_branch=None,
             squash=False,
             edit=False,
             delete_branch=True,
             dry_run=False):
        """Land an individual review request.

        Args:
            destination_branch (unicode):
                The destination branch that the change will be committed or
                merged to.

            review_request (rbtools.api.resource.ReviewRequestResource):
                The review request containing the change to land.

            source_branch (unicode, optional):
                The source branch to land, if landing from a local branch.

            squash (bool, optional):
                Whether to squash the changes on the branch, for repositories
                that support it.

            edit (bool, optional):
                Whether to edit the commit message before landing.

            delete_branch (bool, optional):
                Whether to delete/close the branch, if landing from a local
                branch.

            dry_run (bool, optional):
                Whether to simulate landing without actually changing the
                repository.
        """
        json_data = {
            'review_request': review_request.id,
            'destination_branch': destination_branch,
        }

        if source_branch:
            review_commit_message = extract_commit_message(review_request)
            author = review_request.get_submitter()

            json_data['source_branch'] = source_branch

            if squash:
                self.stdout.write('Squashing branch "%s" into "%s".' %
                                  (source_branch, destination_branch))
                json_data['type'] = 'squash'
            else:
                self.stdout.write('Merging branch "%s" into "%s".' %
                                  (source_branch, destination_branch))
                json_data['type'] = 'merge'

            if not dry_run:
                try:
                    self.tool.merge(target=source_branch,
                                    destination=destination_branch,
                                    message=review_commit_message,
                                    author=author,
                                    squash=squash,
                                    run_editor=edit,
                                    close_branch=delete_branch)
                except MergeError as e:
                    raise CommandError(six.text_type(e))
        else:
            self.stdout.write('Applying patch from review request %s.' %
                              review_request.id)

            if not dry_run:
                self.patch(review_request.id, squash=squash)

        self.stdout.write('Review request %s has landed on "%s".' %
                          (review_request.id, self.options.destination_branch))
        self.json.append('landed_review_requests', json_data)

    def initialize(self):
        """Initialize the command.

        This overrides Command.initialize in order to handle full review
        request URLs on the command line. In this case, we want to parse that
        URL in order to pull the server name and review request ID out of it.

        Raises:
            rbtools.commands.CommandError:
                A review request URL passed in as the review request ID could
                not be parsed correctly or included a bad diff revision.
        """
        review_request_id = self.options.rid

        if review_request_id and review_request_id.startswith('http'):
            server_url, review_request_id, diff_revision = \
                parse_review_request_url(review_request_id)

            if diff_revision and '-' in diff_revision:
                raise CommandError('Interdiff patches are not supported: %s.' %
                                   diff_revision)

            if review_request_id is None:
                raise CommandError('The URL %s does not appear to be a '
                                   'review request.')

            self.options.server = server_url
            self.options.rid = review_request_id

        super(Land, self).initialize()

    def main(self, branch_name=None, *args):
        """Run the command."""
        self.cmd_args = list(args)

        if branch_name:
            self.cmd_args.insert(0, branch_name)

        if not self.tool.can_merge:
            raise CommandError(
                'This command does not support %s repositories.' %
                self.tool.name)

        if self.options.push and not self.tool.can_push_upstream:
            raise CommandError('--push is not supported for %s repositories.' %
                               self.tool.name)

        if self.tool.has_pending_changes():
            raise CommandError('Working directory is not clean.')

        if not self.options.destination_branch:
            raise CommandError('Please specify a destination branch.')

        if not self.tool.can_squash_merges:
            # If the client doesn't support squashing, then never squash.
            self.options.squash = False

        if self.options.rid:
            is_local = branch_name is not None
            review_request_id = self.options.rid
        else:
            try:
                review_request = guess_existing_review_request(
                    api_root=self.api_root,
                    api_client=self.api_client,
                    tool=self.tool,
                    revisions=get_revisions(self.tool, self.cmd_args),
                    guess_summary=False,
                    guess_description=False,
                    is_fuzzy_match_func=self._ask_review_request_match,
                    repository_id=self.repository.id)
            except ValueError as e:
                raise CommandError(six.text_type(e))

            if not review_request or not review_request.id:
                raise CommandError('Could not determine the existing review '
                                   'request URL to land.')

            review_request_id = review_request.id
            is_local = True

        try:
            review_request = self.api_root.get_review_request(
                review_request_id=review_request_id)
        except APIError as e:
            raise CommandError('Error getting review request %s: %s' %
                               (review_request_id, e))

        if self.options.is_local is not None:
            is_local = self.options.is_local

        if is_local:
            if branch_name is None:
                branch_name = self.tool.get_current_branch()

            if branch_name == self.options.destination_branch:
                raise CommandError('The local branch cannot be merged onto '
                                   'itself. Try a different local branch or '
                                   'destination branch.')
        else:
            branch_name = None

        land_error = self.can_land(review_request)

        if land_error is not None:
            raise CommandError('Cannot land review request %s: %s' %
                               (review_request_id, land_error))

        land_kwargs = {
            'delete_branch': self.options.delete_branch,
            'destination_branch': self.options.destination_branch,
            'dry_run': self.options.dry_run,
            'edit': self.options.edit,
            'squash': self.options.squash,
        }

        self.json.add('landed_review_requests', [])

        if self.options.recursive:
            # The dependency graph shows us which review requests depend on
            # which other ones. What we are actually after is the order to land
            # them in, which is the topological sorting order of the converse
            # graph. It just so happens that if we reverse the topological sort
            # of a graph, it is a valid topological sorting of the converse
            # graph, so we don't have to compute the converse graph.
            dependency_graph = review_request.build_dependency_graph()
            dependencies = toposort(dependency_graph)[1:]

            if dependencies:
                self.stdout.write('Recursively landing dependencies of '
                                  'review request %s.' % review_request_id)

                for dependency in dependencies:
                    land_error = self.can_land(dependency)

                    if land_error is not None:
                        raise CommandError(
                            'Aborting recursive land of review request %s.\n'
                            'Review request %s cannot be landed: %s' %
                            (review_request_id, dependency.id, land_error))

                for dependency in reversed(dependencies):
                    self.land(review_request=dependency, **land_kwargs)

        self.land(review_request=review_request,
                  source_branch=branch_name,
                  **land_kwargs)

        if self.options.push:
            self.stdout.write('Pushing branch "%s" upstream' %
                              self.options.destination_branch)

            if not self.options.dry_run:
                try:
                    self.tool.push_upstream(self.options.destination_branch)
                except PushError as e:
                    raise CommandError(six.text_type(e))

    def _ask_review_request_match(self, review_request):
        return confirm('Land Review Request #%s: "%s"? ' %
                       (review_request.id,
                        get_draft_or_current_value('summary', review_request)))
Esempio n. 4
0
class Stamp(Command):
    """Add the review request URL to the last commit message.

    Guesses the existing review request ID and stamp the review request URL
    to the last commit message. If a review request ID is specified by the
    user, use the specified review request URL instead of guessing.
    """
    name = 'stamp'
    author = 'The Review Board Project'

    option_list = [
        OptionGroup(
            name='Stamp Options',
            description='Controls the behavior of a stamp, including what '
            'review request URL gets stamped.',
            option_list=[
                Option('-r',
                       '--review-request-id',
                       dest='rid',
                       metavar='ID',
                       default=None,
                       help='Specifies the existing review request ID to '
                       'be stamped.'),
            ]),
        Command.server_options,
        Command.repository_options,
        Command.diff_options,
    ]

    def no_commit_error(self):
        raise CommandError('No existing commit to stamp on.')

    def _ask_review_request_match(self, review_request):
        question = ("Stamp last commit with Review Request #%s: '%s'? " %
                    (review_request.id,
                     get_draft_or_current_value('summary', review_request)))

        return confirm(question)

    def main(self, *args):
        """Stamp the latest commit with corresponding review request URL"""
        self.cmd_args = list(args)

        repository_info, self.tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(self.tool, api_root=api_root)

        if not self.tool.can_amend_commit:
            raise NotImplementedError('rbt stamp is not supported with %s.' %
                                      self.tool.name)

        try:
            if self.tool.has_pending_changes():
                raise CommandError('Working directory is not clean.')
        except NotImplementedError:
            pass

        revisions = get_revisions(self.tool, self.cmd_args)
        commit_message = self.tool.get_raw_commit_message(revisions)

        if '\nReviewed at http' in commit_message:
            raise CommandError('This commit is already stamped.')

        if not self.options.rid:
            self.options.rid = guess_existing_review_request_id(
                repository_info,
                self.options.repository_name,
                api_root,
                api_client,
                self.tool,
                revisions,
                guess_summary=False,
                guess_description=False,
                is_fuzzy_match_func=self._ask_review_request_match,
                no_commit_error=self.no_commit_error)

            if not self.options.rid:
                raise CommandError('Could not determine the existing review '
                                   'request URL to stamp with.')

        review_request = get_review_request(self.options.rid, api_root)
        stamp_url = review_request.absolute_url
        commit_message += '\n\nReviewed at %s' % stamp_url

        self.tool.amend_commit(commit_message)
        print('Changes committed to current branch.')
Esempio n. 5
0
class Patch(Command):
    """Applies a specific patch from a RB server.

    The patch file indicated by the request id is downloaded from the
    server and then applied locally."""

    name = 'patch'
    author = 'The Review Board Project'
    args = '<review-request-id>'
    option_list = [
        Option('-c',
               '--commit',
               dest='commit',
               action='store_true',
               default=False,
               help='Commits using information fetched '
               'from the review request (Git/Mercurial only).',
               added_in='0.5.3'),
        Option('-C',
               '--commit-no-edit',
               dest='commit_no_edit',
               action='store_true',
               default=False,
               help='Commits using information fetched '
               'from the review request (Git/Mercurial only). '
               'This differs from --commit by not invoking the editor '
               'to modify the commit message.'),
        Option('--diff-revision',
               dest='diff_revision',
               metavar='REVISION',
               default=None,
               help='The Review Board diff revision ID to use for the patch.'),
        Option('--px',
               dest='px',
               metavar='NUM',
               default=None,
               help="Strips the given number of paths from filenames in the "
               "diff. Equivalent to patch's `-p` argument."),
        Option('--print',
               dest='patch_stdout',
               action='store_true',
               default=False,
               help='Prints the patch to standard output instead of applying '
               'it to the tree.',
               added_in='0.5.3'),
        Option('-R',
               '--revert',
               dest='revert_patch',
               action='store_true',
               default=False,
               help='Revert the given patch instead of applying it.\n'
               'This feature does not work with Bazaar or Mercurial '
               'repositories.',
               added_in='0.7.3'),
        Option('--commit-ids',
               dest='commit_ids',
               default=None,
               help='Comma-separated list of commit IDs to apply.\n'
               'This only applies to review requests created with commit '
               'history.',
               added_in='2.0'),
        Option('--squash',
               dest='squash',
               action='store_true',
               default=False,
               help='Squash all patches into one commit. This is only used if '
               'also using -c/--commit or -C/--commit-no-edit.',
               added_in='2.0'),
        Command.server_options,
        Command.repository_options,
    ]

    def get_patches(self,
                    diff_revision=None,
                    commit_ids=None,
                    squashed=False,
                    reverted=False):
        """Return the requested patches and their metadata.

        If a diff revision is not specified, then this will look at the most
        recent diff.

        Args:
            diff_revision (int, optional):
                The diff revision to apply.

                The latest revision will be used if not provided.

            commit_ids (list of unicode, optional):
                The specific commit IDs to apply.

                If not specified, the squashed version of any commits
                (or the sole diff, in a non-multi-commit review request)
                will be applied.

            squashed (bool, optional):
                Whether to return a squashed version of the commits, if
                using a multi-commit review request.

            reverted (bool, optional):
                Return patches in the order needed to revert them.

        Returns:
            list of dict:
            A list of dictionaries with the following keys:

            ``basedir`` (:py:class:`unicode`):
                The base directory of the returned patch.

            ``diff`` (:py:class:`bytes`):
                The actual patch contents.

            ``patch_num`` (:py:class:`int`):
                The application number for the patch. This is 1-based.

            ``revision`` (:py:class:`int`):
                The revision of the returned patch.

            ``commit_meta`` (:py:class:`dict`):
                Metadata about the requested commit if one was requested.
                Otherwise, this will be ``None``.

        Raises:
            rbtools.command.CommandError:
                One of the following occurred:

                * The patch could not be retrieved or does not exist
                * The review request was created without history support and
                  ``commit_ids`` was provided.
                * One or more requested commit IDs could not be found.
        """
        if commit_ids is not None:
            commit_ids = set(commit_ids)

        # Sanity-check the arguments, making sure that the options provided
        # are compatible with each other and with the Review Board server.
        server_supports_history = self._tool.capabilities.has_capability(
            'review_requests', 'supports_history')

        if server_supports_history:
            if squashed and commit_ids:
                logger.warning('--squash is not compatible with --commit-ids; '
                               'ignoring --squash')
                squashed = False
        else:
            squashed = True

            if commit_ids:
                logger.warning('This server does not support review requests '
                               'with history; ignoring --commit-ids=...')
                commit_ids = None

        # If a diff revision is not specified, we'll need to get the latest
        # revision through the API.
        if diff_revision is None:
            try:
                diffs = self._api_root.get_diffs(
                    review_request_id=self._review_request_id,
                    only_fields='',
                    only_links='')
            except APIError as e:
                raise CommandError('Error getting diffs: %s' % e)

            # Use the latest diff if a diff revision was not given.
            # Since diff revisions start a 1, increment by one, and
            # never skip a number, the latest diff revisions number
            # should be equal to the number of diffs.
            diff_revision = diffs.total_results

        try:
            # Fetch the main diff and (unless we're squashing) any commits within.
            if squashed:
                diff = self._api_root.get_diff(
                    review_request_id=self._review_request_id,
                    diff_revision=diff_revision)
            else:
                diff = self._api_root.get_diff(
                    review_request_id=self._review_request_id,
                    diff_revision=diff_revision,
                    expand='commits')
        except APIError:
            raise CommandError('The specified diff revision does not '
                               'exist.')

        # Begin to gather results.
        patches = []

        if squashed or len(diff.commits) == 0:
            # Either this was a review request created before we had
            # multi-commit, or the user requested to squash everything. Return
            # a single patch.

            try:
                diff_content = diff.get_patch().data
            except APIError:
                raise CommandError(
                    _('Unable to retrieve the diff content for revision %s') %
                    diff_revision)

            # We only have one patch to apply, containing a squashed version
            # of all commits.
            patches.append({
                'base_dir': getattr(diff, 'basedir', ''),
                'commit_meta': None,
                'diff': diff_content,
                'patch_num': 1,
                'revision': diff_revision,
            })
        else:
            # We'll be returning one patch per commit. This may be the
            # entire list of the review request, or a filtered list.
            commits = diff.commits

            if commit_ids:
                # Filter the commits down by the specified list of IDs.
                commit_ids = set(commit_ids)
                commits = [
                    commit for commit in commits
                    if commit['commit_id'] in commit_ids
                ]

                # Make sure we're not missing any.
                if len(commits) != len(commit_ids):
                    found_commit_ids = set(commit['commit_id']
                                           for commit in commits)

                    raise CommandError(
                        _('The following commit IDs could not be found: %s') %
                        ', '.join(sorted(commit_ids - found_commit_ids)))

            for patch_num, commit in enumerate(commits, start=1):
                try:
                    diff_content = commit.get_patch().data
                except APIError:
                    raise CommandError(
                        _('Unable to retrieve the diff content for '
                          'revision %(diff_revision)d, commit %(commit_id)s') %
                        {
                            'diff_revision': diff_revision,
                            'commit_id': commit['commit_id'],
                        })

                patches.append({
                    # DiffSets on review requests created with history
                    # support *always* have an empty base dir.
                    'base_dir': '',
                    'commit_meta': {
                        'author':
                        PatchAuthor(full_name=commit.author_name,
                                    email=commit.author_email),
                        'author_date':
                        commit.author_date,
                        'committer_date':
                        commit.committer_date,
                        'committer_email':
                        commit.committer_email,
                        'committer_name':
                        commit.committer_name,
                        'message':
                        commit.commit_message,
                    },
                    'diff': diff_content,
                    'patch_num': patch_num,
                    'revision': diff_revision,
                })

        if reverted:
            patches = list(reversed(patches))

        return patches

    def apply_patch(self,
                    diff_file_path,
                    base_dir,
                    patch_num,
                    total_patches,
                    revert=False):
        """Apply a patch to the tree.

        Args:
            diff_file_path (unicode):
                The file path of the diff being applied.

            base_dir (unicode):
                The base directory within which to apply the patch.

            patch_num (int):
                The 1-based index of the patch being applied.

            total_patches (int):
                The total number of patches being applied.

            revert (bool, optional):
                Whether the patch is being reverted.

        Returns:
            bool:
            ``True`` if the patch was applied/reverted successfully.
            ``False`` if the patch was partially applied/reverted but there
            were conflicts.

        Raises:
            rbtools.command.CommandError:
                There was an error applying or reverting the patch.
        """
        # If we're working with more than one patch, show the patch number
        # we're applying or reverting. If we're only working with one, the
        # previous log from _apply_patches() will suffice.
        if total_patches > 1:
            if revert:
                msg = _('Reverting patch %(num)d/%(total)d...')
            else:
                msg = _('Applying patch %(num)d/%(total)d...')

            logger.info(msg, {
                'num': patch_num,
                'total': total_patches,
            })

        result = self._tool.apply_patch(
            patch_file=diff_file_path,
            base_path=self._repository_info.base_path,
            base_dir=base_dir,
            p=self.options.px,
            revert=revert)

        if result.patch_output:
            print()

            patch_output = result.patch_output.strip()

            if six.PY2:
                print(patch_output)
            else:
                sys.stdout.buffer.write(patch_output)
                print()

            print()

        if not result.applied:
            if revert:
                raise CommandError(
                    'Unable to revert the patch. The patch may be invalid, or '
                    'there may be conflicts that could not be resolved.')
            else:
                raise CommandError(
                    'Unable to apply the patch. The patch may be invalid, or '
                    'there may be conflicts that could not be resolved.')

        if result.has_conflicts:
            if result.conflicting_files:
                if revert:
                    print('The patch was partially reverted, but there were '
                          'conflicts in:')
                else:
                    print('The patch was partially applied, but there were '
                          'conflicts in:')

                print()

                for filename in result.conflicting_files:
                    print('    %s' % filename)

                print()
            elif revert:
                print('The patch was partially reverted, but there were '
                      'conflicts.')
            else:
                print('The patch was partially applied, but there were '
                      'conflicts.')

            return False

        return True

    def main(self, review_request_id):
        """Run the command.

        Args:
            review_request_id (int):
                The ID of the review request to patch from.

        Raises:
            rbtools.command.CommandError:
                Patching the tree has failed.
        """
        patch_stdout = self.options.patch_stdout
        revert = self.options.revert_patch

        if patch_stdout and revert:
            raise CommandError(_('--print and --revert cannot both be used.'))

        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type,
            require_repository_info=not patch_stdout)

        server_url = self.get_server_url(repository_info, tool)
        diff_revision = None

        if review_request_id.startswith('http'):
            (server_url, review_request_id,
             diff_revision) = parse_review_request_url(review_request_id)

            if diff_revision and '-' in diff_revision:
                raise CommandError('Interdiff patches not supported: %s.' %
                                   diff_revision)

        if diff_revision is None:
            diff_revision = self.options.diff_revision

        if revert and not tool.supports_patch_revert:
            raise CommandError(
                _('The %s backend does not support reverting patches.') %
                tool.name)

        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)

        if not patch_stdout:
            # Check if the repository info on the Review Board server matches
            # the local checkout.
            repository_info = repository_info.find_server_repository_info(
                api_root)

            # Check if the working directory is clean.
            try:
                if tool.has_pending_changes():
                    message = 'Working directory is not clean.'

                    if self.options.commit:
                        raise CommandError(message)
                    else:
                        logger.warning(message)
            except NotImplementedError:
                pass

        # Store the instances we've set up so that other commands have access.
        self._api_root = api_root
        self._repository_info = repository_info
        self._tool = tool
        self._review_request_id = review_request_id

        if self.options.commit_ids:
            # Do our best to normalize what gets passed in, so that we don't
            # end up with any blank entries.
            commit_ids = [
                commit_id for commit_id in COMMIT_ID_SPLIT_RE.split(
                    self.options.commit_ids.trim()) if commit_id
            ]
        else:
            commit_ids = None

        # Fetch the patches from the review request, based on the requested
        # options.
        patches = self.get_patches(diff_revision=diff_revision,
                                   commit_ids=commit_ids,
                                   squashed=self.options.squash,
                                   reverted=revert)

        if patch_stdout:
            self._output_patches(patches)
        else:
            self._apply_patches(patches)

    def _output_patches(self, patches):
        """Output the contents of the patches to the console.

        Args:
            patches (list of dict):
                The list of patches that would be applied.
        """
        for patch_data in patches:
            diff_body = patch_data['diff']

            if isinstance(diff_body, bytes):
                if six.PY3:
                    sys.stdout.buffer.write(diff_body)
                    print()
                else:
                    print(diff_body.decode('utf-8'))
            else:
                print(diff_body)

    def _apply_patches(self, patches):
        """Apply a list of patches to the tree.

        Args:
            patches (list of dict):
                The list of patches to apply.

        Raises:
            rbtools.command.CommandError:
                Patching the tree has failed.
        """
        squash = self.options.squash
        revert = self.options.revert_patch
        commit_no_edit = self.options.commit_no_edit
        will_commit = self.options.commit or commit_no_edit
        total_patches = len(patches)

        # Check if we're planning to commit and have any patch without
        # metadata, in which case we'll need to fetch the review request so we
        # can generate a commit message.
        needs_review_request = will_commit and (
            squash or total_patches == 1
            or any(patch_data['commit_meta'] is None
                   for patch_data in patches))

        if needs_review_request:
            # Fetch the review request to use as a description. We only
            # want to fetch this once.
            try:
                review_request = self._api_root.get_review_request(
                    review_request_id=self._review_request_id,
                    force_text_type='plain')
            except APIError as e:
                raise CommandError(
                    _('Error getting review request %(review_request_id)d: '
                      '%(error)s') % {
                          'review_request_id': self._review_request_id,
                          'error': e,
                      })

            default_author = review_request.get_submitter()
            default_commit_message = extract_commit_message(review_request)
        else:
            default_author = None
            default_commit_message = None

        # Display a summary of what's about to be applied.
        diff_revision = patches[0]['revision']

        if revert:
            summary = ngettext(
                ('Reverting 1 patch from review request '
                 '%(review_request_id)s (diff revision %(diff_revision)s)'),
                ('Reverting %(num)d patches from review request '
                 '%(review_request_id)s (diff revision %(diff_revision)s)'),
                total_patches)
        else:
            summary = ngettext(
                ('Applying 1 patch from review request '
                 '%(review_request_id)s (diff revision %(diff_revision)s)'),
                ('Applying %(num)d patches from review request '
                 '%(review_request_id)s (diff revision %(diff_revision)s)'),
                total_patches)

        logger.info(
            summary, {
                'num': total_patches,
                'review_request_id': self._review_request_id,
                'diff_revision': diff_revision,
            })

        # Start applying all the patches.
        for patch_data in patches:
            patch_num = patch_data['patch_num']
            tmp_patch_file = make_tempfile(patch_data['diff'])

            success = self.apply_patch(diff_file_path=tmp_patch_file,
                                       base_dir=patch_data['base_dir'],
                                       patch_num=patch_num,
                                       total_patches=total_patches,
                                       revert=revert)

            os.unlink(tmp_patch_file)

            if not success:
                if revert:
                    error = _('Could not apply patch %(num)d of %(total)d')
                else:
                    error = _('Could not revert patch %(num)d of %(total)d')

                raise CommandError(error % {
                    'num': patch_num,
                    'total': total_patches,
                })

            # If the user wants to commit, then we'll be committing every
            # patch individually, unless the user wants to squash commits in
            # which case we'll only do this on the final commit.
            if will_commit and (not squash or patch_num == total_patches):
                meta = patch_data.get('commit_meta')

                if meta is not None and not squash and total_patches > 1:
                    # We are patching a commit so we already have the metadata
                    # required without making additional HTTP requests.
                    message = meta['message']
                    author = meta['author']
                else:
                    # We'll build this based on the summary/description from
                    # the review request and the patch number.
                    message = default_commit_message
                    author = default_author

                    assert message is not None
                    assert author is not None

                    if total_patches > 1:
                        # Record the patch number to help differentiate, in
                        # case we only have review request information and
                        # not commit messages. In practice, this shouldn't
                        # happen, as we should always have commit messages,
                        # but it's a decent safeguard.
                        message = '[%s/%s] %s' % (patch_num, total_patches,
                                                  message)

                if revert:
                    # Make it clear that this commit is reverting a prior
                    # patch, so it's easy to identify.
                    message = '[Revert] %s' % message

                try:
                    self._tool.create_commit(message=message,
                                             author=author,
                                             run_editor=not commit_no_edit)
                except CreateCommitError as e:
                    raise CommandError(six.text_type(e))
                except NotImplementedError:
                    raise CommandError('--commit is not supported with %s' %
                                       self._tool.name)
Esempio n. 6
0
class Patch(Command):
    """Applies a specific patch from a RB server.

    The patch file indicated by the request id is downloaded from the
    server and then applied locally."""
    name = "patch"
    author = "The Review Board Project"
    args = "<review-request-id>"
    option_list = [
        Option("--diff-revision",
               dest="diff_revision",
               default=None,
               help="revision id of diff to be used as patch"),
        Option("--px",
               dest="px",
               default=None,
               help="numerical pX argument for patch"),
        Option("--print",
               dest="patch_stdout",
               action="store_true",
               default=False,
               help="print patch to stdout instead of applying"),
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
        Option('--repository-type',
               dest='repository_type',
               config_key="REPOSITORY_TYPE",
               default=None,
               help='the type of repository in the current directory. '
               'In most cases this should be detected '
               'automatically but some directory structures '
               'containing multiple repositories require this '
               'option to select the proper type. Valid '
               'values include bazaar, clearcase, cvs, git, '
               'mercurial, perforce, plastic, and svn.'),
    ]

    def get_patch(self, request_id, api_root, diff_revision=None):
        """Return the diff as a string, the used diff revision and its basedir.

        If a diff revision is not specified, then this will look at the most
        recent diff.
        """
        try:
            diffs = api_root.get_diffs(review_request_id=request_id)
        except APIError, e:
            raise CommandError("Error getting diffs: %s" % e)

        # Use the latest diff if a diff revision was not given.
        # Since diff revisions start a 1, increment by one, and
        # never skip a number, the latest diff revisions number
        # should be equal to the number of diffs.
        if diff_revision is None:
            diff_revision = diffs.total_results

        try:
            diff = diffs.get_item(diff_revision)
            diff_body = diff.get_patch().data
            base_dir = diff.basedir
        except APIError:
            raise CommandError('The specified diff revision does not exist.')

        return diff_body, diff_revision, base_dir
Esempio n. 7
0
class Post(Command):
    """Create and update review requests."""
    name = "post"
    author = "The Review Board Project"
    args = "[changenum]"
    option_list = [
        Option("-r",
               "--review-request-id",
               dest="rid",
               metavar="ID",
               default=None,
               help="existing review request ID to update"),
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("--disable-proxy",
               action='store_false',
               dest='enable_proxy',
               config_key="ENABLE_PROXY",
               default=True,
               help="prevents requests from going through a proxy server"),
        Option('-p',
               '--publish',
               dest="publish",
               action="store_true",
               default=False,
               help="publish the review request immediately after submitting"),
        Option("--target-groups",
               dest="target_groups",
               config_key="TARGET_GROUPS",
               default=None,
               help="names of the groups who will perform the review"),
        Option("--target-people",
               dest="target_people",
               config_key="TARGET_PEOPLE",
               default=None,
               help="names of the people who will perform the review"),
        Option("--summary",
               dest="summary",
               default=None,
               help="summary of the review "),
        Option("--description",
               dest="description",
               default=None,
               help="description of the review "),
        Option("--description-file",
               dest="description_file",
               default=None,
               help="text file containing a description of the review"),
        Option('-g',
               '--guess-fields',
               dest="guess_fields",
               action="store_true",
               config_key="GUESS_FIELDS",
               default=False,
               help="equivalent to --guess-summary --guess-description"),
        Option("--guess-summary",
               dest="guess_summary",
               action="store_true",
               config_key="GUESS_SUMMARY",
               default=False,
               help="guess summary from the latest commit "
               "(bzr/git/hg/hgsubversion only)"),
        Option("--guess-description",
               dest="guess_description",
               action="store_true",
               config_key="GUESS_DESCRIPTION",
               default=False,
               help="guess description based on commits on this branch "
               "(bzr/git/hg/hgsubversion only)"),
        Option("--testing-done",
               dest="testing_done",
               default=None,
               help="details of testing done "),
        Option("--testing-done-file",
               dest="testing_file",
               default=None,
               help="text file containing details of testing done "),
        Option("--branch",
               dest="branch",
               config_key="BRANCH",
               default=None,
               help="affected branch "),
        Option("--bugs-closed",
               dest="bugs_closed",
               default=None,
               help="list of bugs closed "),
        Option("--change-description",
               default=None,
               help="description of what changed in this revision of "
               "the review request when updating an existing request"),
        Option("--revision-range",
               dest="revision_range",
               default=None,
               help="generate the diff for review based on given "
               "revision range"),
        Option("--submit-as",
               dest="submit_as",
               metavar="USERNAME",
               config_key="SUBMIT_AS",
               default=None,
               help="user name to be recorded as the author of the "
               "review request, instead of the logged in user"),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
        Option("--change-only",
               dest="change_only",
               action="store_true",
               default=False,
               help="updates info from changelist, but does "
               "not upload a new diff (only available if your "
               "repository supports changesets)"),
        Option("--parent",
               dest="parent_branch",
               metavar="PARENT_BRANCH",
               config_key="PARENT_BRANCH",
               default=None,
               help="the parent branch this diff should be against "
               "(only available if your repository supports "
               "parent diffs)"),
        Option("--tracking-branch",
               dest="tracking",
               metavar="TRACKING",
               config_key="TRACKING_BRANCH",
               default=None,
               help="Tracking branch from which your branch is derived "
               "(git only, defaults to origin/master)"),
        Option("--p4-client",
               dest="p4_client",
               config_key="P4_CLIENT",
               default=None,
               help="the Perforce client name that the review is in"),
        Option("--p4-port",
               dest="p4_port",
               config_key="P4_PORT",
               default=None,
               help="the Perforce servers IP address that the review is on"),
        Option("--p4-passwd",
               dest="p4_passwd",
               config_key="P4_PASSWD",
               default=None,
               help="the Perforce password or ticket of the user "
               "in the P4USER environment variable"),
        Option("--svn-changelist",
               dest="svn_changelist",
               default=None,
               help="generate the diff for review based on a local SVN "
               "changelist"),
        Option("--repository-url",
               dest="repository_url",
               config_key="REPOSITORY",
               default=None,
               help="the url for a repository for creating a diff "
               "outside of a working copy (currently only "
               "supported by Subversion with --revision-range or "
               "--diff-filename and ClearCase with relative "
               "paths outside the view). For git, this specifies"
               "the origin url of the current repository, "
               "overriding the origin url supplied by the git "
               "client."),
        Option("-d",
               "--debug",
               action="store_true",
               dest="debug",
               config_key="DEBUG",
               default=False,
               help="display debug output"),
        Option("--diff-filename",
               dest="diff_filename",
               default=None,
               help="upload an existing diff file, instead of "
               "generating a new diff"),
        Option("--http-username",
               dest="http_username",
               metavar="USERNAME",
               config_key="HTTP_USERNAME",
               default=None,
               help="username for HTTP Basic authentication"),
        Option("--http-password",
               dest="http_password",
               metavar="PASSWORD",
               config_key="HTTP_PASSWORD",
               default=None,
               help="password for HTTP Basic authentication"),
        Option("--basedir",
               dest="basedir",
               default=None,
               help="the absolute path in the repository the diff was "
               "generated in. Will override the detected path."),
    ]

    def post_process_options(self):
        if self.options.debug:
            logging.getLogger().setLevel(logging.DEBUG)

        if self.options.description and self.options.description_file:
            sys.stderr.write("The --description and --description-file "
                             "options are mutually exclusive.\n")
            sys.exit(1)

        if self.options.description_file:
            if os.path.exists(self.options.description_file):
                fp = open(self.options.description_file, "r")
                self.options.description = fp.read()
                fp.close()
            else:
                sys.stderr.write("The description file %s does not exist.\n" %
                                 self.options.description_file)
                sys.exit(1)

        if self.options.guess_fields:
            self.options.guess_summary = True
            self.options.guess_description = True

        if self.options.testing_done and self.options.testing_file:
            sys.stderr.write("The --testing-done and --testing-done-file "
                             "options are mutually exclusive.\n")
            sys.exit(1)

        if self.options.testing_file:
            if os.path.exists(self.options.testing_file):
                fp = open(self.options.testing_file, "r")
                self.options.testing_done = fp.read()
                fp.close()
            else:
                sys.stderr.write("The testing file %s does not exist.\n" %
                                 self.options.testing_file)
                sys.exit(1)

    def get_repository_path(self, repository_info, api_root):
        """Get the repository path from the server.

        This will compare the paths returned by the SCM client
        with those one the server, and return the first match.
        """
        if isinstance(repository_info.path, list):
            repositories = api_root.get_repositories()

            try:
                while True:
                    for repo in repositories:
                        if repo['path'] in repository_info.path:
                            repository_info.path = repo['path']
                            raise StopIteration()

                    repositories = repositories.get_next()
            except StopIteration:
                pass

        if isinstance(repository_info.path, list):
            sys.stderr.write('\n')
            sys.stderr.write('There was an error creating this review '
                             'request.\n')
            sys.stderr.write('\n')
            sys.stderr.write('There was no matching repository path'
                             'found on the server.\n')

            sys.stderr.write('Unknown repository paths found:\n')

            for foundpath in repository_info.path:
                sys.stderr.write('\t%s\n' % foundpath)

            sys.stderr.write('Ask the administrator to add one of '
                             'these repositories\n')
            sys.stderr.write('to the Review Board server.\n')
            die()

        return repository_info.path

    def post_request(self,
                     tool,
                     repository_info,
                     server_url,
                     api_root,
                     changenum=None,
                     diff_content=None,
                     parent_diff_content=None,
                     submit_as=None,
                     retries=3):
        """Creates or updates a review request, and uploads a diff.

        On success the review request id and url are returned.
        """
        if self.options.rid:
            # Retrieve the review request coresponding to the user
            # provided id.
            try:
                review_request = api_root.get_review_request(
                    review_request_id=self.options.rid)
            except APIError, e:
                die("Error getting review request %s: %s" %
                    (self.options.rid, e))

            if review_request.status == 'submitted':
                die("Review request %s is marked as %s. In order to "
                    "update it, please reopen the request and try again." %
                    (self.options.rid, review_request.status))
        else:
Esempio n. 8
0
class Publish(Command):
    """Publish a specific review request from a draft."""

    name = 'publish'
    author = 'The Review Board Project'
    args = '<review-request-id>'
    option_list = [
        Command.server_options,
        Command.repository_options,
        Option('-t',
               '--trivial',
               dest='trivial_publish',
               action='store_true',
               default=False,
               help='Publish the review request without sending an e-mail '
               'notification.',
               added_in='1.0'),
        Option('--markdown',
               dest='markdown',
               action='store_true',
               config_key='MARKDOWN',
               default=False,
               help='Specifies if the change description should should be '
               'interpreted as Markdown-formatted text.',
               added_in='1.0'),
        Option('-m',
               '--change-description',
               dest='change_description',
               default=None,
               help='The change description to use for the publish.',
               added_in='1.0'),
    ]

    def main(self, review_request_id):
        """Run the command."""
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)

        try:
            review_request = api_root.get_review_request(
                review_request_id=review_request_id,
                only_fields='public',
                only_links='draft')
        except APIError as e:
            raise CommandError('Error getting review request %s: %s' %
                               (review_request_id, e))

        self.setup_tool(tool, api_root)

        update_fields = {
            'public': True,
        }

        if (self.options.trivial_publish and tool.capabilities.has_capability(
                'review_requests', 'trivial_publish')):
            update_fields['trivial'] = True

        if self.options.change_description is not None:
            if review_request.public:
                update_fields['changedescription'] = \
                    self.options.change_description

                if (self.options.markdown and tool.capabilities.has_capability(
                        'text', 'markdown')):
                    update_fields['changedescription_text_type'] = 'markdown'
                else:
                    update_fields['changedescription_text_type'] = 'plain'
            else:
                logging.error(
                    'The change description field can only be set when '
                    'publishing an update.')

        try:
            draft = review_request.get_draft(only_fields='')
            draft.update(**update_fields)
        except APIError as e:
            raise CommandError('Error publishing review request (it may '
                               'already be published): %s' % e)

        print('Review request #%s is published.' % review_request_id)
Esempio n. 9
0
import subprocess
import sys
from optparse import OptionParser

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

from rbtools import get_version_string
from rbtools.commands import Option, RB_MAIN


GLOBAL_OPTIONS = [
    Option('-h', '--help',
           action='store_true',
           dest='help',
           default=False).make_option({})
]


def build_help_text(command_class):
    """Generate help text from a command class."""
    command = command_class()
    parser = command.create_parser({})
    help_file = StringIO()
    parser.print_help(help_file)
    help_text = help_file.getvalue()
    help_file.close()
    return help_text

Esempio n. 10
0
class Status(Command):
    """Display review requests for the current repository."""
    name = "status"
    author = "The Review Board Project"
    description = "Output a list of your pending review requests."
    args = ""
    option_list = [
        Option("--all",
               dest="all_repositories",
               action="store_true",
               default=False,
               help="Show review requests for all repositories instead "
               "of the detected repository."),
        Command.server_options,
        Command.repository_options,
        Command.perforce_options,
    ]

    def output_request(self, request):
        print "   r/%s - %s" % (request.id, request.summary)

    def output_draft(self, request, draft):
        print " * r/%s - %s" % (request.id, draft.summary)

    def main(self):
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)
        user = get_user(api_client, api_root, auth_required=True)

        query_args = {
            'from_user': user.username,
            'status': 'pending',
            'expand': 'draft',
        }

        if not self.options.all_repositories:
            repo_id = get_repository_id(repository_info,
                                        api_root,
                                        repository_name=self.config.get(
                                            'REPOSITORY', None))

            if repo_id:
                query_args['repository'] = repo_id
            else:
                logging.warning('The repository detected in the current '
                                'directory was not found on\n'
                                'the Review Board server. Displaying review '
                                'requests from all repositories.')

        requests = api_root.get_review_requests(**query_args)

        try:
            while True:
                for request in requests:
                    if request.draft:
                        self.output_draft(request, request.draft[0])
                    else:
                        self.output_request(request)

                requests = requests.get_next(**query_args)
        except StopIteration:
            pass
Esempio n. 11
0
class Patch(Command):
    """Applies a specific patch from a RB server.

    The patch file indicated by the request id is downloaded from the
    server and then applied locally."""
    name = "patch"
    author = "The Review Board Project"
    args = "<review-request-id>"
    option_list = [
        Option("-c",
               "--commit",
               dest="commit",
               action="store_true",
               default=False,
               help="Commit using information fetched "
               "from the review request (Git/Mercurial only)."),
        Option("-C",
               "--commit-no-edit",
               dest="commit_no_edit",
               action="store_true",
               default=False,
               help="Commit using information fetched "
               "from the review request (Git/Mercurial only). "
               "This differs from -c by not invoking the editor "
               "to modify the commit message."),
        Option("--diff-revision",
               dest="diff_revision",
               default=None,
               help="revision id of diff to be used as patch"),
        Option("--px",
               dest="px",
               default=None,
               help="numerical pX argument for patch"),
        Option("--print",
               dest="patch_stdout",
               action="store_true",
               default=False,
               help="print patch to stdout instead of applying"),
        Command.server_options,
        Command.repository_options,
    ]

    def get_patch(self, request_id, api_root, diff_revision=None):
        """Return the diff as a string, the used diff revision and its basedir.

        If a diff revision is not specified, then this will look at the most
        recent diff.
        """
        try:
            diffs = api_root.get_diffs(review_request_id=request_id)
        except APIError, e:
            raise CommandError("Error getting diffs: %s" % e)

        # Use the latest diff if a diff revision was not given.
        # Since diff revisions start a 1, increment by one, and
        # never skip a number, the latest diff revisions number
        # should be equal to the number of diffs.
        if diff_revision is None:
            diff_revision = diffs.total_results

        try:
            diff = diffs.get_item(diff_revision)
            diff_body = diff.get_patch().data
            base_dir = getattr(diff, 'basedir', None) or ''
        except APIError:
            raise CommandError('The specified diff revision does not exist.')

        return diff_body, diff_revision, base_dir
Esempio n. 12
0
class Stamp(Command):
    """Add the review request URL to the commit message.

    Stamps the review request URL onto the commit message of the revision
    specified. The revisions argument behaves like it does in rbt post, where
    it is required for some SCMs (e.g. Perforce) and unnecessary/ignored for
    others (e.g. Git).

    Normally, this command will guess the review request (based on the revision
    number if provided, and the commit summary and description otherwise).
    However, if a review request ID is specified by the user, it stamps the URL
    of that review request instead of guessing.
    """
    name = 'stamp'
    author = 'The Review Board Project'
    description = 'Adds the review request URL to the commit message.'
    args = '[revisions]'

    option_list = [
        OptionGroup(
            name='Stamp Options',
            description='Controls the behavior of a stamp, including what '
                        'review request URL gets stamped.',
            option_list=[
                Option('-r', '--review-request-id',
                       dest='rid',
                       metavar='ID',
                       default=None,
                       help='Specifies the existing review request ID to '
                            'be stamped.'),
            ]
        ),
        Command.server_options,
        Command.repository_options,
        Command.diff_options,
        Command.branch_options,
        Command.perforce_options,
    ]

    def no_commit_error(self):
        raise CommandError('No existing commit to stamp on.')

    def _ask_review_request_match(self, review_request):
        question = ("Stamp with Review Request #%s: '%s'? "
                    % (review_request.id,
                       get_draft_or_current_value(
                           'summary', review_request)))

        return confirm(question)

    def determine_review_request(self, api_client, api_root, repository_info,
                                 repository_name, revisions):
        """Determine the correct review request for a commit.

        A tuple (review request ID, review request absolute URL) is returned.
        If no review request ID is found by any of the strategies,
        (None, None) is returned.
        """
        # First, try to match the changeset to a review request directly.
        if repository_info.supports_changesets:
            review_request = find_review_request_by_change_id(
                api_client, api_root, repository_info, repository_name,
                revisions)

            if review_request and review_request.id:
                return review_request.id, review_request.absolute_url

        # Fall back on guessing based on the description. This may return None
        # if no suitable review request is found.
        logging.debug('Attempting to guess review request based on '
                      'summary and description')
        review_request = guess_existing_review_request(
            repository_info, repository_name, api_root, api_client, self.tool,
            revisions, guess_summary=False, guess_description=False,
            is_fuzzy_match_func=self._ask_review_request_match,
            no_commit_error=self.no_commit_error)

        if review_request:
            logging.debug('Found review request ID %d' % review_request.id)
            return review_request.id, review_request.absolute_url
        else:
            logging.debug('Could not find a matching review request')
            return None, None

    def main(self, *args):
        """Add the review request URL to a commit message."""
        self.cmd_args = list(args)

        repository_info, self.tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(self.tool, api_root=api_root)

        if not self.tool.can_amend_commit:
            raise NotImplementedError('rbt stamp is not supported with %s.'
                                      % self.tool.name)

        try:
            if self.tool.has_pending_changes():
                raise CommandError('Working directory is not clean.')
        except NotImplementedError:
            pass

        revisions = get_revisions(self.tool, self.cmd_args)

        # Use the ID from the command line options if present.
        if self.options.rid:
            review_request = get_review_request(self.options.rid, api_root)
            review_request_id = self.options.rid
            review_request_url = review_request.absolute_url
        else:
            review_request_id, review_request_url = \
                self. determine_review_request(
                    api_client, api_root, repository_info,
                    self.options.repository_name, revisions)

        if not review_request_url:
            raise CommandError('Could not determine the existing review '
                               'request URL to stamp with.')

        stamp_commit_with_review_url(revisions, review_request_url, self.tool)

        print('Successfully stamped change with the URL:')
        print(review_request_url)
Esempio n. 13
0
class Status(Command):
    """Display review requests for the current repository."""

    name = 'status'
    author = 'The Review Board Project'
    description = 'Output a list of your pending review requests.'
    args = '[review-request [revision]]'
    option_list = [
        Option('--format',
               dest='format',
               default=None,
               help='Set the output format. The format is in the form of '
               '%%(field_name)s, where field_name is one of: id, status,'
               'summary, or description.\n'
               'A character escape can be included via \\xXX where XX is '
               'the hex code of a character.\n'
               'For example: --format="%%(id)s\\x09%%(summary)s"\n'
               'This option will print out the id and summary tab-'
               'separated.'),
        Option('-z',
               dest='format_nul',
               default=False,
               action='store_true',
               help='Null-terminate each entry. Otherwise, the entries will '
               'be newline-terminated.'),
        Option('--all',
               dest='all_repositories',
               action='store_true',
               default=False,
               help='Shows review requests for all repositories instead '
               'of just the detected repository.'),
        Command.server_options,
        Command.repository_options,
        Command.perforce_options,
        Command.tfs_options,
    ]
    # The number of spaces between the request's status and the request's id
    # and summary.
    TAB_SIZE = 3
    # The number of spaces after the end of the request's summary.
    PADDING = 5

    _HEX_RE = re.compile(r'\\x([0-9a-fA-f]{2})')

    def tabulate(self, review_requests):
        """Print review request summary and status in a table.

        Args:
            review_requests (list of dict):
                A list that contains statistics about each review request.
        """
        if len(review_requests):
            has_branches = False
            has_bookmarks = False

            table = tt.Texttable(get_terminal_size().columns)
            header = ['Status', 'Review Request']

            for info in review_requests:
                if 'branch' in info:
                    has_branches = True

                if 'bookmark' in info:
                    has_bookmarks = True

            if has_branches:
                header.append('Branch')

            if has_bookmarks:
                header.append('Bookmark')

            table.header(header)

            for info in review_requests:
                row = [
                    info['status'],
                    'r/%s - %s' % (info['id'], info['summary']),
                ]

                if has_branches:
                    row.append(info.get('branch') or '')

                if has_bookmarks:
                    row.append(info.get('bookmark') or '')

                table.add_row(row)

            print(table.draw())
        else:
            print('No review requests found.')

        print()

    def get_data(self, requests):
        """Return current status and review summary for all reviews.

        Args:
            requests (ListResource):
                A ListResource that contains data on all open/draft requests.

        Returns:
            list: A list whose elements are dicts of each request's statistics.
        """
        requests_stats = []

        for request in requests.all_items:
            if request.draft:
                status = 'Draft'
            elif request.issue_open_count:
                status = 'Open Issues (%s)' % request.issue_open_count
            elif request.ship_it_count:
                status = 'Ship It! (%s)' % request.ship_it_count
            else:
                status = 'Pending'

            info = {
                'id': request.id,
                'status': status,
                'summary': request.summary,
                'description': request.description,
            }

            if 'local_branch' in request.extra_data:
                info['branch'] = \
                    request.extra_data['local_branch']
            elif 'local_bookmark' in request.extra_data:
                info['bookmark'] = \
                    request.extra_data['local_bookmark']

            requests_stats.append(info)

        return requests_stats

    def main(self):
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)
        username = get_username(api_client, api_root, auth_required=True)

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        query_args = {
            'from_user': username,
            'status': 'pending',
            'expand': 'draft',
        }

        if not self.options.all_repositories:
            repo_id = get_repository_id(
                repository_info,
                api_root,
                repository_name=self.options.repository_name)

            if repo_id:
                query_args['repository'] = repo_id
            else:
                logging.warning('The repository detected in the current '
                                'directory was not found on\n'
                                'the Review Board server. Displaying review '
                                'requests from all repositories.')

        review_requests = api_root.get_review_requests(**query_args)
        review_request_info = self.get_data(review_requests)

        if self.options.format:
            self.format_results(review_request_info)
        else:
            self.tabulate(review_request_info)

    def format_results(self, review_requests):
        """Print formatted information about the review requests.

        Args:
            review_requests (list of dict):
                The information about the review requests.
        """
        fmt = self._HEX_RE.sub(
            lambda m: chr(int(m.group(1), 16)),
            self.options.format,
        )

        if self.options.format_nul:
            end = '\x00'
        else:
            end = '\n'

        for info in review_requests:
            print(fmt % info, end=end)
Esempio n. 14
0
class Status(Command):
    """Display review requests for the current repository."""
    name = 'status'
    author = 'The Review Board Project'
    description = 'Output a list of your pending review requests.'
    args = ''
    option_list = [
        Option('--all',
               dest='all_repositories',
               action='store_true',
               default=False,
               help='Shows review requests for all repositories instead '
               'of just the detected repository.'),
        Command.server_options,
        Command.repository_options,
        Command.perforce_options,
        Command.tfs_options,
    ]

    def output_request(self, request):
        print('   r/%s - %s' % (request.id, request.summary))

    def output_draft(self, request, draft):
        print(' * r/%s - %s' % (request.id, draft.summary))

    def main(self):
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)
        username = get_username(api_client, api_root, auth_required=True)

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        query_args = {
            'from_user': username,
            'status': 'pending',
            'expand': 'draft',
        }

        if not self.options.all_repositories:
            repo_id = get_repository_id(
                repository_info,
                api_root,
                repository_name=self.options.repository_name)

            if repo_id:
                query_args['repository'] = repo_id
            else:
                logging.warning('The repository detected in the current '
                                'directory was not found on\n'
                                'the Review Board server. Displaying review '
                                'requests from all repositories.')

        requests = api_root.get_review_requests(**query_args)

        for request in requests.all_items:
            if request.draft:
                self.output_draft(request, request.draft[0])
            else:
                self.output_request(request)
Esempio n. 15
0
class Patch(Command):
    """Applies a specific patch from a RB server.

    The patch file indicated by the request id is downloaded from the
    server and then applied locally."""
    name = 'patch'
    author = 'The Review Board Project'
    args = '<review-request-id>'
    option_list = [
        Option('-c', '--commit',
               dest='commit',
               action='store_true',
               default=False,
               help='Commits using information fetched '
                    'from the review request (Git/Mercurial only).',
               added_in='0.5.3'),
        Option('-C', '--commit-no-edit',
               dest='commit_no_edit',
               action='store_true',
               default=False,
               help='Commits using information fetched '
                    'from the review request (Git/Mercurial only). '
                    'This differs from --commit by not invoking the editor '
                    'to modify the commit message.'),
        Option('--diff-revision',
               dest='diff_revision',
               metavar='REVISION',
               default=None,
               help='The Review Board diff revision ID to use for the patch.'),
        Option('--px',
               dest='px',
               metavar='NUM',
               default=None,
               help="Strips the given number of paths from filenames in the "
                    "diff. Equivalent to patch's `-p` argument."),
        Option('--print',
               dest='patch_stdout',
               action='store_true',
               default=False,
               help='Prints the patch to standard output instead of applying '
                    'it to the tree.',
               added_in='0.5.3'),
        Option('-R', '--revert',
               dest='revert_patch',
               action='store_true',
               default=False,
               help='Revert the given patch instead of applying it.\n'
                    'This feature does not work with Bazaar or Mercurial '
                    'repositories.',
               added_in='0.7.3'),
        Command.server_options,
        Command.repository_options,
    ]

    def get_patch(self, request_id, api_root, diff_revision=None):
        """Return the diff as a string, the used diff revision and its basedir.

        If a diff revision is not specified, then this will look at the most
        recent diff.
        """
        try:
            diffs = api_root.get_diffs(review_request_id=request_id)
        except APIError as e:
            raise CommandError('Error getting diffs: %s' % e)

        # Use the latest diff if a diff revision was not given.
        # Since diff revisions start a 1, increment by one, and
        # never skip a number, the latest diff revisions number
        # should be equal to the number of diffs.
        if diff_revision is None:
            diff_revision = diffs.total_results

        try:
            diff = diffs.get_item(diff_revision)
            diff_body = diff.get_patch().data
            base_dir = getattr(diff, 'basedir', None) or ''
        except APIError:
            raise CommandError('The specified diff revision does not exist.')

        return diff_body, diff_revision, base_dir

    def apply_patch(self, repository_info, tool, request_id, diff_revision,
                    diff_file_path, base_dir, revert=False):
        """Apply patch patch_file and display results to user."""
        if revert:
            print('Patch is being reverted from request %s with diff revision '
                  '%s.' % (request_id, diff_revision))
        else:
            print('Patch is being applied from request %s with diff revision '
                  '%s.' % (request_id, diff_revision))

        result = tool.apply_patch(diff_file_path, repository_info.base_path,
                                  base_dir, self.options.px, revert=revert)

        if result.patch_output:
            print()
            print(result.patch_output.strip())
            print()

        if not result.applied:
            if revert:
                raise CommandError(
                    'Unable to revert the patch. The patch may be invalid, or '
                    'there may be conflicts that could not be resolved.')
            else:
                raise CommandError(
                    'Unable to apply the patch. The patch may be invalid, or '
                    'there may be conflicts that could not be resolved.')

        if result.has_conflicts:
            if result.conflicting_files:
                if revert:
                    print('The patch was partially reverted, but there were '
                          'conflicts in:')
                else:
                    print('The patch was partially applied, but there were '
                          'conflicts in:')

                print()

                for filename in result.conflicting_files:
                    print('    %s' % filename)

                print()
            elif revert:
                print('The patch was partially reverted, but there were '
                      'conflicts.')
            else:
                print('The patch was partially applied, but there were '
                      'conflicts.')

            return False
        else:
            if revert:
                print('Successfully reverted patch.')
            else:
                print('Successfully applied patch.')

            return True

    def main(self, request_id):
        """Run the command."""
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)

        if self.options.revert_patch and not tool.supports_patch_revert:
            raise CommandError('The %s backend does not support reverting '
                               'patches.' % tool.name)

        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        # Get the patch, the used patch ID and base dir for the diff
        diff_body, diff_revision, base_dir = self.get_patch(
            request_id,
            api_root,
            self.options.diff_revision)

        if self.options.patch_stdout:
            print(diff_body)
        else:
            try:
                if tool.has_pending_changes():
                    message = 'Working directory is not clean.'

                    if not self.options.commit:
                        print('Warning: %s' % message)
                    else:
                        raise CommandError(message)
            except NotImplementedError:
                pass

            tmp_patch_file = make_tempfile(diff_body)

            success = self.apply_patch(repository_info, tool, request_id,
                                       diff_revision, tmp_patch_file, base_dir,
                                       revert=self.options.revert_patch)

            if success and (self.options.commit or
                            self.options.commit_no_edit):
                try:
                    review_request = api_root.get_review_request(
                        review_request_id=request_id,
                        force_text_type='plain')
                except APIError as e:
                    raise CommandError('Error getting review request %s: %s'
                                       % (request_id, e))

                message = extract_commit_message(review_request)
                author = review_request.get_submitter()

                try:
                    tool.create_commit(message, author,
                                       not self.options.commit_no_edit)
                    print('Changes committed to current branch.')
                except NotImplementedError:
                    raise CommandError('--commit is not supported with %s'
                                       % tool.name)
Esempio n. 16
0
class Post(Command):
    """Create and update review requests."""
    name = "post"
    author = "The Review Board Project"
    description = "Uploads diffs to create and update review requests."
    args = "[revisions]"

    GUESS_AUTO = 'auto'
    GUESS_YES = 'yes'
    GUESS_NO = 'no'
    GUESS_YES_INPUT_VALUES = (True, 'yes', 1, '1')
    GUESS_NO_INPUT_VALUES = (False, 'no', 0, '0')
    GUESS_CHOICES = (GUESS_AUTO, GUESS_YES, GUESS_NO)

    option_list = [
        OptionGroup(
            name='Posting Options',
            description='Controls the behavior of a post, including what '
            'review request gets posted and how, and what '
            'happens after it is posted.',
            option_list=[
                Option('-r',
                       '--review-request-id',
                       dest='rid',
                       metavar='ID',
                       default=None,
                       help='Specifies the existing review request ID to '
                       'update.'),
                Option('-u',
                       '--update',
                       dest='update',
                       action='store_true',
                       default=False,
                       help='Automatically determines the existing review '
                       'request to update.'),
                Option('-p',
                       '--publish',
                       dest='publish',
                       action='store_true',
                       default=False,
                       config_key='PUBLISH',
                       help='Immediately publishes the review request after '
                       'posting.'),
                Option('-o',
                       '--open',
                       dest='open_browser',
                       action='store_true',
                       config_key='OPEN_BROWSER',
                       default=False,
                       help='Opens a web browser to the review request '
                       'after posting.'),
                Option('--submit-as',
                       dest='submit_as',
                       metavar='USERNAME',
                       config_key='SUBMIT_AS',
                       default=None,
                       help='The user name to use as the author of the '
                       'review request, instead of the logged in user.'),
                Option('--change-only',
                       dest='change_only',
                       action='store_true',
                       default=False,
                       help='Updates fields from the change description, '
                       'but does not upload a new diff '
                       '(Perforce/Plastic only).'),
                Option('--diff-only',
                       dest='diff_only',
                       action='store_true',
                       default=False,
                       help='Uploads a new diff, but does not update '
                       'fields from the change description '
                       '(Perforce/Plastic only).'),
            ]),
        Command.server_options,
        Command.repository_options,
        OptionGroup(
            name='Review Request Field Options',
            description='Options for setting the contents of fields in the '
            'review request.',
            option_list=[
                Option('-g',
                       '--guess-fields',
                       dest='guess_fields',
                       action='store',
                       config_key='GUESS_FIELDS',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Short-hand for --guess-summary '
                       '--guess-description.'),
                Option('--guess-summary',
                       dest='guess_summary',
                       action='store',
                       config_key='GUESS_SUMMARY',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Generates the Summary field based on the '
                       'commit messages (Bazaar/Git/Mercurial only).'),
                Option('--guess-description',
                       dest='guess_description',
                       action='store',
                       config_key='GUESS_DESCRIPTION',
                       nargs='?',
                       default=GUESS_AUTO,
                       const=GUESS_YES,
                       choices=GUESS_CHOICES,
                       help='Generates the Description field based on the '
                       'commit messages (Bazaar/Git/Mercurial only).'),
                Option('--change-description',
                       default=None,
                       help='A description of what changed in this update '
                       'of the review request. This is ignored for new '
                       'review requests.'),
                Option('--summary',
                       dest='summary',
                       default=None,
                       help='The new contents for the Summary field.'),
                Option('--description',
                       dest='description',
                       default=None,
                       help='The new contents for the Description field.'),
                Option('--description-file',
                       dest='description_file',
                       default=None,
                       metavar='FILENAME',
                       help='A text file containing the new contents for the '
                       'Description field.'),
                Option('--testing-done',
                       dest='testing_done',
                       default=None,
                       help='The new contents for the Testing Done field.'),
                Option('--testing-done-file',
                       dest='testing_file',
                       default=None,
                       metavar='FILENAME',
                       help='A text file containing the new contents for the '
                       'Testing Done field.'),
                Option('--branch',
                       dest='branch',
                       config_key='BRANCH',
                       default=None,
                       help='The branch the change will be committed on.'),
                Option('--bugs-closed',
                       dest='bugs_closed',
                       default=None,
                       help='The comma-separated list of bug IDs closed.'),
                Option('--target-groups',
                       dest='target_groups',
                       config_key='TARGET_GROUPS',
                       default=None,
                       help='The names of the groups that should perform the '
                       'review.'),
                Option('--target-people',
                       dest='target_people',
                       config_key='TARGET_PEOPLE',
                       default=None,
                       help='The usernames of the people who should perform '
                       'the review.'),
                Option('--depends-on',
                       dest='depends_on',
                       config_key='DEPENDS_ON',
                       default=None,
                       help='The new contents for the Depends On field.'),
                Option('--markdown',
                       dest='markdown',
                       action='store_true',
                       config_key='MARKDOWN',
                       default=False,
                       help='Specifies if the summary and description should '
                       'be interpreted as Markdown-formatted text '
                       '(Review Board 2.0+ only).'),
            ]),
        Command.diff_options,
        Command.perforce_options,
        Command.subversion_options,
    ]

    def post_process_options(self):
        # -g implies --guess-summary and --guess-description
        if self.options.guess_fields:
            self.options.guess_fields = self.normalize_guess_value(
                self.options.guess_fields, '--guess-fields')

            self.options.guess_summary = self.options.guess_fields
            self.options.guess_description = self.options.guess_fields

        if self.options.revision_range:
            raise CommandError(
                'The --revision-range argument has been removed. To post a '
                'diff for one or more specific revisions, pass those '
                'revisions as arguments. For more information, see the '
                'RBTools 0.6 Release Notes.')

        if self.options.svn_changelist:
            raise CommandError(
                'The --svn-changelist argument has been removed. To use a '
                'Subversion changelist, pass the changelist name as an '
                'additional argument after the command.')

        # Only one of --description and --description-file can be used
        if self.options.description and self.options.description_file:
            raise CommandError("The --description and --description-file "
                               "options are mutually exclusive.\n")

        # If --description-file is used, read that file
        if self.options.description_file:
            if os.path.exists(self.options.description_file):
                fp = open(self.options.description_file, "r")
                self.options.description = fp.read()
                fp.close()
            else:
                raise CommandError(
                    "The description file %s does not exist.\n" %
                    self.options.description_file)

        # Only one of --testing-done and --testing-done-file can be used
        if self.options.testing_done and self.options.testing_file:
            raise CommandError("The --testing-done and --testing-done-file "
                               "options are mutually exclusive.\n")

        # If --testing-done-file is used, read that file
        if self.options.testing_file:
            if os.path.exists(self.options.testing_file):
                fp = open(self.options.testing_file, "r")
                self.options.testing_done = fp.read()
                fp.close()
            else:
                raise CommandError("The testing file %s does not exist.\n" %
                                   self.options.testing_file)

        # If we have an explicitly specified summary, override
        # --guess-summary
        if self.options.summary:
            self.options.guess_summary = self.GUESS_NO
        else:
            self.options.guess_summary = self.normalize_guess_value(
                self.options.guess_summary, '--guess-summary')

        # If we have an explicitly specified description, override
        # --guess-description
        if self.options.description:
            self.options.guess_description = self.GUESS_NO
        else:
            self.options.guess_description = self.normalize_guess_value(
                self.options.guess_description, '--guess-description')

        # If we have an explicitly specified review request ID, override
        # --update
        if self.options.rid and self.options.update:
            self.options.update = False

    def normalize_guess_value(self, guess, arg_name):
        if guess in self.GUESS_YES_INPUT_VALUES:
            return self.GUESS_YES
        elif guess in self.GUESS_NO_INPUT_VALUES:
            return self.GUESS_NO
        elif guess == self.GUESS_AUTO:
            return guess
        else:
            raise CommandError('Invalid value "%s" for argument "%s"' %
                               (guess, arg_name))

    def get_repository_path(self, repository_info, api_root):
        """Get the repository path from the server.

        This will compare the paths returned by the SCM client
        with those one the server, and return the first match.
        """
        if isinstance(repository_info.path, list):
            repositories = api_root.get_repositories()

            try:
                while True:
                    for repo in repositories:
                        if repo['path'] in repository_info.path:
                            repository_info.path = repo['path']
                            raise StopIteration()

                    repositories = repositories.get_next()
            except StopIteration:
                pass

        if isinstance(repository_info.path, list):
            error_str = [
                'There was an error creating this review request.\n',
                '\n',
                'There was no matching repository path found on the server.\n',
                'Unknown repository paths found:\n',
            ]

            for foundpath in repository_info.path:
                error_str.append('\t%s\n' % foundpath)

            error_str += [
                'Ask the administrator to add one of these repositories\n',
                'to the Review Board server.\n',
            ]

            raise CommandError(''.join(error_str))

        return repository_info.path

    def get_draft_or_current_value(self, field_name, review_request):
        """Returns the draft or current field value from a review request.

        If a draft exists for the supplied review request, return the draft's
        field value for the supplied field name, otherwise return the review
        request's field value for the supplied field name.
        """
        if review_request.draft:
            fields = review_request.draft[0]
        else:
            fields = review_request

        return fields[field_name]

    def get_possible_matches(self,
                             review_requests,
                             summary,
                             description,
                             limit=5):
        """Returns a sorted list of tuples of score and review request.

        Each review request is given a score based on the summary and
        description provided. The result is a sorted list of tuples containing
        the score and the corresponding review request, sorted by the highest
        scoring review request first.
        """
        candidates = []

        # Get all potential matches.
        try:
            while True:
                for review_request in review_requests:
                    summary_pair = (self.get_draft_or_current_value(
                        'summary', review_request), summary)
                    description_pair = (self.get_draft_or_current_value(
                        'description', review_request), description)
                    score = Score.get_match(summary_pair, description_pair)
                    candidates.append((score, review_request))

                review_requests = review_requests.get_next()
        except StopIteration:
            pass

        # Sort by summary and description on descending rank.
        sorted_candidates = sorted(
            candidates,
            key=lambda m: (m[0].summary_score, m[0].description_score),
            reverse=True)

        return sorted_candidates[:limit]

    def num_exact_matches(self, possible_matches):
        """Returns the number of exact matches in the possible match list."""
        count = 0

        for score, request in possible_matches:
            if score.is_exact_match():
                count += 1

        return count

    def guess_existing_review_request_id(self, repository_info, api_root,
                                         api_client):
        """Try to guess the existing review request ID if it is available.

        The existing review request is guessed by comparing the existing
        summary and description to the current post's summary and description,
        respectively. The current post's summary and description are guessed if
        they are not provided.

        If the summary and description exactly match those of an existing
        review request, the ID for which is immediately returned. Otherwise,
        the user is prompted to select from a list of potential matches,
        sorted by the highest ranked match first.
        """
        user = get_user(api_client, api_root, auth_required=True)
        repository_id = get_repository_id(repository_info, api_root,
                                          self.options.repository_name)

        try:
            # Get only pending requests by the current user for this
            # repository.
            review_requests = api_root.get_review_requests(
                repository=repository_id,
                from_user=user.username,
                status='pending',
                expand='draft')

            if not review_requests:
                raise CommandError('No existing review requests to update for '
                                   'user %s.' % user.username)
        except APIError, e:
            raise CommandError('Error getting review requests for user '
                               '%s: %s' % (user.username, e))

        summary = self.options.summary
        description = self.options.description

        if not summary or not description:
            try:
                commit_message = self.get_commit_message()

                if commit_message:
                    if not summary:
                        summary = commit_message['summary']

                    if not description:
                        description = commit_message['description']
            except NotImplementedError:
                raise CommandError('--summary and --description are required.')

        possible_matches = self.get_possible_matches(review_requests, summary,
                                                     description)
        exact_match_count = self.num_exact_matches(possible_matches)

        for score, review_request in possible_matches:
            # If the score is the only exact match, return the review request
            # ID without confirmation, otherwise prompt.
            if score.is_exact_match() and exact_match_count == 1:
                return review_request.id
            else:
                question = ("Update Review Request #%s: '%s'? " %
                            (review_request.id,
                             self.get_draft_or_current_value(
                                 'summary', review_request)))

                if confirm(question):
                    return review_request.id

        return None
Esempio n. 17
0
class Alias(Command):
    """A command for managing aliases defined in .reviewboardrc files."""

    name = 'alias'
    author = 'The Review Board Project'
    description = 'Manage aliases defined in .reviewboardrc files.'
    option_list = [
        Option('--list',
               action='store_true',
               dest='list_aliases',
               default=False,
               help='List all aliases defined in .reviewboardrc files.'),
        Option('--dry-run',
               metavar='ALIAS',
               dest='dry_run_alias',
               default=None,
               help='Print the command as it would be executed with the given '
               'command-line arguments.'),
    ]

    def list_aliases(self):
        """Print a list of .reviewboardrc aliases to the command line.

        This function shows in which file each alias is defined in and if the
        alias is valid (that is, if it won't be executable because an rbt
        command exists with the same name).
        """
        # A mapping of configuration file paths to aliases.
        aliases = defaultdict(dict)

        # A mapping of aliases to configuration file paths. This allows us to
        # determine where aliases are overridden.
        predefined_aliases = {}

        config_paths = get_config_paths()

        for config_path in config_paths:
            config = parse_config_file(config_path)

            if 'ALIASES' in config:
                for alias_name, alias_cmd in six.iteritems(config['ALIASES']):
                    predefined = alias_name in predefined_aliases

                    aliases[config_path][alias_name] = {
                        'command': alias_cmd,
                        'overridden': predefined,
                        'invalid': command_exists(alias_name),
                    }

                    if not predefined:
                        predefined_aliases[alias_name] = config_path

        for config_path in config_paths:
            if aliases[config_path]:
                self.stdout.write('[%s]' % config_path)

                for alias_name, entry in six.iteritems(aliases[config_path]):
                    self.stdout.write('    %s = %s' %
                                      (alias_name, entry['command']))

                    if entry['invalid']:
                        self.stdout.write('      !! This alias is overridden '
                                          'by an rbt command !!')
                    elif entry['overridden']:
                        self.stdout.write('      !! This alias is overridden '
                                          'by another alias in "%s" !!' %
                                          predefined_aliases[alias_name])
                self.stdout.new_line()

    def main(self, *args):
        """Run the command."""
        if ((self.options.list_aliases and self.options.dry_run_alias) or
                not (self.options.list_aliases or self.options.dry_run_alias)):
            raise CommandError('You must provide exactly one of --list or '
                               '--dry-run.')

        if self.options.list_aliases:
            self.list_aliases()
        elif self.options.dry_run_alias:
            try:
                alias = self.config['ALIASES'][self.options.dry_run_alias]
            except KeyError:
                raise CommandError('No such alias "%s"' %
                                   self.options.dry_run_alias)

            command = expand_alias(alias, args)[0]

            self.stdout.write(list2cmdline(command))
Esempio n. 18
0
class Land(Command):
    """Land changes from a review request onto the remote repository.

    This command takes a review request, applies it to a feature branch,
    merges it with the specified destination branch, and pushes the
    changes to an upstream repository.

    Notes:
        The review request needs to be approved first.

        ``--local`` option can be used to skip the patching step.
    """
    name = 'land'
    author = 'The Review Board Project'
    args = '[<branch-name>]'
    option_list = [
        Option('--dest',
               dest='destination_branch',
               default=None,
               config_key='LAND_DEST_BRANCH',
               help='Specifies the destination branch to land changes on.'),
        Option('-r',
               '--review-request-id',
               dest='rid',
               metavar='ID',
               default=None,
               help='Specifies the review request ID.'),
        Option('--local',
               dest='is_local',
               action='store_true',
               default=None,
               help='Forces the change to be merged without patching, if '
               'merging a local branch. Defaults to true unless '
               '--review-request-id is used.'),
        Option('-p',
               '--push',
               dest='push',
               action='store_true',
               default=False,
               config_key='LAND_PUSH',
               help='Pushes the branch after landing the change.'),
        Option('-n',
               '--no-push',
               dest='push',
               action='store_false',
               default=False,
               config_key='LAND_PUSH',
               help='Prevents pushing the branch after landing the change, '
               'if pushing is enabled by default.'),
        Option('--squash',
               dest='squash',
               action='store_true',
               default=False,
               config_key='LAND_SQUASH',
               help='Squashes history into a single commit.'),
        Option(
            '--no-squash',
            dest='squash',
            action='store_false',
            default=False,
            config_key='LAND_SQUASH',
            help='Disables squashing history into a single commit, choosing '
            'instead to merge the branch, if squashing is enabled by '
            'default.'),
        Option('-e',
               '--edit',
               dest='edit',
               action='store_true',
               default=False,
               help='Invokes the editor to edit the commit message before '
               'landing the change.'),
        Option('--delete-branch',
               dest='delete_branch',
               action='store_true',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Deletes the local branch after it's landed. Only used if "
               "landing a local branch. This is the default."),
        Option('--no-delete-branch',
               dest='delete_branch',
               action='store_false',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Prevents the local branch from being deleted after it's "
               "landed."),
        Option('--dry-run',
               dest='dry_run',
               action='store_true',
               default=False,
               help='Simulates the landing of a change, without actually '
               'making any changes to the tree.'),
        Command.server_options,
        Command.repository_options,
    ]

    def patch(self, review_request_id):
        patch_command = [RB_MAIN, 'patch']
        patch_command.extend(build_rbtools_cmd_argv(self.options))

        if self.options.edit:
            patch_command.append('-c')
        else:
            patch_command.append('-C')

        patch_command.append(review_request_id)

        p = subprocess.Popen(patch_command)
        rc = p.wait()

        if rc:
            die('Failed to execute command: %s' % patch_command)

    def main(self, branch_name=None, *args):
        """Run the command."""
        self.cmd_args = list(args)

        if branch_name:
            self.cmd_args.insert(0, branch_name)

        repository_info, self.tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(self.tool, api_root=api_root)

        dry_run = self.options.dry_run

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        if (not self.tool.can_merge or not self.tool.can_push_upstream
                or not self.tool.can_delete_branch):
            raise CommandError(
                "This command does not support %s repositories." %
                self.tool.name)

        if self.tool.has_pending_changes():
            raise CommandError('Working directory is not clean.')

        if self.options.rid:
            request_id = self.options.rid
            is_local = branch_name is not None
        else:
            request_id = guess_existing_review_request_id(
                repository_info,
                self.options.repository_name,
                api_root,
                api_client,
                self.tool,
                get_revisions(self.tool, self.cmd_args),
                guess_summary=False,
                guess_description=False,
                is_fuzzy_match_func=self._ask_review_request_match)

            if not request_id:
                raise CommandError('Could not determine the existing review '
                                   'request URL to land.')

            is_local = True

        if self.options.is_local is not None:
            is_local = self.options.is_local

        destination_branch = self.options.destination_branch

        if not destination_branch:
            raise CommandError('Please specify a destination branch.')

        if is_local:
            if branch_name is None:
                branch_name = self.tool.get_current_branch()

            if branch_name == destination_branch:
                raise CommandError('The local branch cannot be merged onto '
                                   'itself. Try a different local branch or '
                                   'destination branch.')

        review_request = get_review_request(request_id, api_root)

        try:
            is_rr_approved = review_request.approved
            approval_failure = review_request.approval_failure
        except AttributeError:
            # The Review Board server is an old version (pre-2.0) that
            # doesn't support the `approved` field. Determining it manually.
            if review_request.ship_it_count == 0:
                is_rr_approved = False
                approval_failure = \
                    'The review request has not been marked "Ship It!"'
            else:
                is_rr_approved = True
        finally:
            if not is_rr_approved:
                raise CommandError(approval_failure)

        if is_local:
            review_commit_message = extract_commit_message(review_request)
            author = review_request.get_submitter()

            if self.options.squash:
                print('Squashing branch "%s" into "%s"' %
                      (branch_name, destination_branch))
            else:
                print('Merging branch "%s" into "%s"' %
                      (branch_name, destination_branch))

            if not dry_run:
                try:
                    self.tool.merge(branch_name, destination_branch,
                                    review_commit_message, author,
                                    self.options.squash, self.options.edit)
                except MergeError as e:
                    raise CommandError(str(e))

            if self.options.delete_branch:
                print('Deleting merged branch "%s"' % branch_name)

                if not dry_run:
                    self.tool.delete_branch(branch_name, merged_only=False)
        else:
            print('Applying patch from review request %s' % request_id)

            if not dry_run:
                self.patch(request_id)

        if self.options.push:
            print('Pushing branch "%s" upstream' % destination_branch)

            if not dry_run:
                try:
                    self.tool.push_upstream(destination_branch)
                except PushError as e:
                    raise CommandError(str(e))

        print('Review request %s has landed on "%s".' %
              (request_id, destination_branch))

    def _ask_review_request_match(self, review_request):
        return confirm('Land Review Request #%s: "%s"? ' %
                       (review_request.id,
                        get_draft_or_current_value('summary', review_request)))
Esempio n. 19
0
class Status(Command):
    """Display review requests for the current repository."""
    name = "status"
    author = "The Review Board Project"
    description = "Output a list of your pending review requests."
    args = ""
    option_list = [
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
        Option("--all",
               dest="all_repositories",
               action="store_true",
               default=False,
               help="Show review requests for all repositories instead "
               "of the detected repository."),
        Option('--repository-type',
               dest='repository_type',
               config_key="REPOSITORY_TYPE",
               default=None,
               help='the type of repository in the current directory. '
               'In most cases this should be detected '
               'automatically but some directory structures '
               'containing multiple repositories require this '
               'option to select the proper type. Valid '
               'values include bazaar, clearcase, cvs, git, '
               'mercurial, perforce, plastic, and svn.'),
    ]

    def output_request(self, request):
        print "   r/%s - %s" % (request.id, request.summary)

    def output_draft(self, request, draft):
        print " * r/%s - %s" % (request.id, draft.summary)

    def main(self):
        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)
        user = get_user(api_client, api_root, auth_required=True)

        query_args = {
            'from_user': user.username,
            'status': 'pending',
            'expand': 'draft',
        }

        if not self.options.all_repositories:
            repo_id = get_repository_id(repository_info,
                                        api_root,
                                        repository_name=self.config.get(
                                            'REPOSITORY', None))

            if repo_id:
                query_args['repository'] = repo_id
            else:
                logging.warning('The repository detected in the current '
                                'directory was not found on\n'
                                'the Review Board server. Displaying review '
                                'requests from all repositories.')

        requests = api_root.get_review_requests(**query_args)

        try:
            while True:
                for request in requests:
                    if request.draft:
                        self.output_draft(request, request.draft[0])
                    else:
                        self.output_request(request)

                requests = requests.get_next(**query_args)
        except StopIteration:
            pass
Esempio n. 20
0
import argparse
import os
import signal
import subprocess
import sys

import pkg_resources

from rbtools import get_version_string
from rbtools.commands import find_entry_point_for_command, Option, RB_MAIN
from rbtools.utils.aliases import run_alias
from rbtools.utils.filesystem import load_config

GLOBAL_OPTIONS = [
    Option('-v',
           '--version',
           action='version',
           version='RBTools %s' % get_version_string()),
    Option('-h', '--help', action='store_true', dest='help', default=False),
    Option('command',
           nargs=argparse.REMAINDER,
           help='The RBTools command to execute, and any arguments. '
           '(See below)'),
]


def build_help_text(command_class):
    """Generate help text from a command class."""
    command = command_class()
    parser = command.create_parser({})

    return parser.format_help()
Esempio n. 21
0
class Close(Command):
    """Close a specific review request as discarded or submitted.

    By default, the command will change the status to submitted. The
    user can provide an optional description for this action.
    """

    name = 'close'
    author = 'The Review Board Project'

    needs_api = True

    args = '<review-request-id>'
    option_list = [
        Option('--close-type',
               dest='close_type',
               default=SUBMITTED,
               help='Either `submitted` or `discarded`.'),
        Option('--description',
               dest='description',
               default=None,
               help='An optional description accompanying the change.'),
        Command.server_options,
        Command.repository_options,
    ]

    def check_valid_type(self, close_type):
        """Check if the user specificed a proper type.

        Type must either be 'discarded' or 'submitted'. If the type
        is wrong, the command will stop and alert the user.
        """
        if close_type not in (SUBMITTED, DISCARDED):
            raise CommandError('%s is not valid type. Try "%s" or "%s"' %
                               (self.options.close_type, SUBMITTED, DISCARDED))

    def main(self, review_request_id):
        """Run the command."""
        close_type = self.options.close_type
        self.check_valid_type(close_type)

        try:
            review_request = self.api_root.get_review_request(
                review_request_id=review_request_id)
        except APIError as e:
            raise CommandError('Error getting review request %s: %s' %
                               (review_request_id, e))

        if review_request.status == close_type:
            raise CommandError('Review request #%s is already %s.' %
                               (review_request_id, close_type))

        if self.options.description:
            review_request = review_request.update(
                status=close_type, description=self.options.description)
        else:
            review_request = review_request.update(status=close_type)

        self.stdout.write('Review request #%s is set to %s.' %
                          (review_request_id, review_request.status))
        self.json.add('review_request', review_request_id)
        self.json.add('status', review_request.status)
Esempio n. 22
0
class Diff(Command):
    """Prints a diff to the terminal."""
    name = "diff"
    author = "The Review Board Project"
    args = "[changenum]"
    option_list = [
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("--revision-range",
               dest="revision_range",
               default=None,
               help="generate the diff for review based on given "
               "revision range"),
        Option("--parent",
               dest="parent_branch",
               metavar="PARENT_BRANCH",
               help="the parent branch this diff should be against "
               "(only available if your repository supports "
               "parent diffs)"),
        Option("--tracking-branch",
               dest="tracking",
               metavar="TRACKING",
               help="Tracking branch from which your branch is derived "
               "(git only, defaults to origin/master)"),
        Option("--svn-show-copies-as-adds",
               dest="svn_show_copies_as_adds",
               metavar="y/n",
               default=None,
               help="don't diff copied or moved files with their source"),
        Option('--svn-changelist',
               dest='svn_changelist',
               default=None,
               help='generate the diff for review based on a local SVN '
               'changelist'),
        Option("--repository-url",
               dest="repository_url",
               help="the url for a repository for creating a diff "
               "outside of a working copy (currently only "
               "supported by Subversion with --revision-range or "
               "--diff-filename and ClearCase with relative "
               "paths outside the view). For git, this specifies"
               "the origin url of the current repository, "
               "overriding the origin url supplied by the git "
               "client."),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
        Option('--repository-type',
               dest='repository_type',
               config_key="REPOSITORY_TYPE",
               default=None,
               help='the type of repository in the current directory. '
               'In most cases this should be detected '
               'automatically but some directory structures '
               'containing multiple repositories require this '
               'option to select the proper type. Valid '
               'values include bazaar, clearcase, cvs, git, '
               'mercurial, perforce, plastic, and svn.'),
    ]

    def main(self, *args):
        """Print the diff to terminal."""
        # The 'args' tuple must be made into a list for some of the
        # SCM Clients code. See comment in post.
        args = list(args)

        repository_info, tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(tool, api_root=api_root)

        diff, parent_diff = get_diff(
            tool,
            repository_info,
            revision_range=self.options.revision_range,
            svn_changelist=self.options.svn_changelist,
            files=args)

        if diff:
            print diff
Esempio n. 23
0
class Land(Command):
    """Land changes from a review request onto the remote repository.

    This command takes a review request, applies it to a feature branch,
    merges it with the specified destination branch, and pushes the
    changes to an upstream repository.

    Notes:
        The review request needs to be approved first.

        ``--local`` option can be used to skip the patching step.
    """

    name = 'land'
    author = 'The Review Board Project'
    args = '[<branch-name>]'
    option_list = [
        Option('--dest',
               dest='destination_branch',
               default=None,
               config_key='LAND_DEST_BRANCH',
               help='Specifies the destination branch to land changes on.'),
        Option('-r',
               '--review-request-id',
               dest='rid',
               metavar='ID',
               default=None,
               help='Specifies the review request ID.'),
        Option('--local',
               dest='is_local',
               action='store_true',
               default=None,
               help='Forces the change to be merged without patching, if '
               'merging a local branch. Defaults to true unless '
               '--review-request-id is used.'),
        Option('-p',
               '--push',
               dest='push',
               action='store_true',
               default=False,
               config_key='LAND_PUSH',
               help='Pushes the branch after landing the change.'),
        Option('-n',
               '--no-push',
               dest='push',
               action='store_false',
               default=False,
               config_key='LAND_PUSH',
               help='Prevents pushing the branch after landing the change, '
               'if pushing is enabled by default.'),
        Option('--squash',
               dest='squash',
               action='store_true',
               default=False,
               config_key='LAND_SQUASH',
               help='Squashes history into a single commit.'),
        Option('--no-squash',
               dest='squash',
               action='store_false',
               default=False,
               config_key='LAND_SQUASH',
               help='Disables squashing history into a single commit, '
               'choosing instead to merge the branch, if squashing is '
               'enabled by default.'),
        Option('-e',
               '--edit',
               dest='edit',
               action='store_true',
               default=False,
               help='Invokes the editor to edit the commit message before '
               'landing the change.'),
        Option('--delete-branch',
               dest='delete_branch',
               action='store_true',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Deletes the local branch after it's landed. Only used if "
               "landing a local branch. This is the default."),
        Option('--no-delete-branch',
               dest='delete_branch',
               action='store_false',
               config_key='LAND_DELETE_BRANCH',
               default=True,
               help="Prevents the local branch from being deleted after it's "
               "landed."),
        Option('--dry-run',
               dest='dry_run',
               action='store_true',
               default=False,
               help='Simulates the landing of a change, without actually '
               'making any changes to the tree.'),
        Option('--recursive',
               dest='recursive',
               action='store_true',
               default=False,
               help='Recursively fetch patches for review requests that the '
               'specified review request depends on. This is equivalent '
               'to calling "rbt patch" for each of those review '
               'requests.',
               added_in='0.8.0'),
        Command.server_options,
        Command.repository_options,
        Command.branch_options,
    ]

    def patch(self, review_request_id):
        """Patch a single review request's diff using rbt patch."""
        patch_command = [RB_MAIN, 'patch']
        patch_command.extend(build_rbtools_cmd_argv(self.options))

        if self.options.edit:
            patch_command.append('-c')
        else:
            patch_command.append('-C')

        patch_command.append(six.text_type(review_request_id))

        rc, output = execute(patch_command,
                             ignore_errors=True,
                             return_error_code=True)

        if rc:
            raise CommandError('Failed to execute "rbt patch":\n%s' % output)

    def can_land(self, review_request):
        """Determine if the review request is land-able.

        A review request can be landed if it is approved or, if the Review
        Board server does not keep track of approval, if the review request
        has a ship-it count.

        This function returns the error with landing the review request or None
        if it can be landed.
        """
        try:
            is_rr_approved = review_request.approved
            approval_failure = review_request.approval_failure
        except AttributeError:
            # The Review Board server is an old version (pre-2.0) that
            # doesn't support the `approved` field. Determine it manually.
            if review_request.ship_it_count == 0:
                is_rr_approved = False
                approval_failure = \
                    'The review request has not been marked "Ship It!"'
            else:
                is_rr_approved = True
        except Exception as e:
            logging.exception(
                'Unexpected error while looking up review request '
                'approval state: %s', e)

            return ('An error was encountered while executing the land '
                    'command.')
        finally:
            if not is_rr_approved:
                return approval_failure

        return None

    def land(self,
             destination_branch,
             review_request,
             source_branch=None,
             squash=False,
             edit=False,
             delete_branch=True,
             dry_run=False):
        """Land an individual review request."""
        if source_branch:
            review_commit_message = extract_commit_message(review_request)
            author = review_request.get_submitter()

            if squash:
                print('Squashing branch "%s" into "%s".' %
                      (source_branch, destination_branch))
            else:
                print('Merging branch "%s" into "%s".' %
                      (source_branch, destination_branch))

            if not dry_run:
                try:
                    self.tool.merge(source_branch, destination_branch,
                                    review_commit_message, author, squash,
                                    edit)
                except MergeError as e:
                    raise CommandError(six.text_type(e))

            if delete_branch:
                print('Deleting merged branch "%s".' % source_branch)

                if not dry_run:
                    self.tool.delete_branch(source_branch, merged_only=False)
        else:
            print('Applying patch from review request %s.' % review_request.id)

            if not dry_run:
                self.patch(review_request.id)

        print('Review request %s has landed on "%s".' %
              (review_request.id, self.options.destination_branch))

    def main(self, branch_name=None, *args):
        """Run the command."""
        self.cmd_args = list(args)

        if branch_name:
            self.cmd_args.insert(0, branch_name)

        repository_info, self.tool = self.initialize_scm_tool(
            client_name=self.options.repository_type)
        server_url = self.get_server_url(repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)
        self.setup_tool(self.tool, api_root=api_root)

        # Check if repository info on reviewboard server match local ones.
        repository_info = repository_info.find_server_repository_info(api_root)

        if (not self.tool.can_merge or not self.tool.can_push_upstream
                or not self.tool.can_delete_branch):
            raise CommandError(
                'This command does not support %s repositories.' %
                self.tool.name)

        if self.tool.has_pending_changes():
            raise CommandError('Working directory is not clean.')

        if not self.options.destination_branch:
            raise CommandError('Please specify a destination branch.')

        if self.options.rid:
            is_local = branch_name is not None
            review_request_id = self.options.rid
        else:
            review_request = guess_existing_review_request(
                repository_info,
                self.options.repository_name,
                api_root,
                api_client,
                self.tool,
                get_revisions(self.tool, self.cmd_args),
                guess_summary=False,
                guess_description=False,
                is_fuzzy_match_func=self._ask_review_request_match)

            if not review_request or not review_request.id:
                raise CommandError('Could not determine the existing review '
                                   'request URL to land.')

            review_request_id = review_request.id
            is_local = True

        review_request = get_review_request(review_request_id, api_root)

        if self.options.is_local is not None:
            is_local = self.options.is_local

        if is_local:
            if branch_name is None:
                branch_name = self.tool.get_current_branch()

            if branch_name == self.options.destination_branch:
                raise CommandError('The local branch cannot be merged onto '
                                   'itself. Try a different local branch or '
                                   'destination branch.')
        else:
            branch_name = None

        land_error = self.can_land(review_request)

        if land_error is not None:
            raise CommandError('Cannot land review request %s: %s' %
                               (review_request_id, land_error))

        if self.options.recursive:
            # The dependency graph shows us which review requests depend on
            # which other ones. What we are actually after is the order to land
            # them in, which is the topological sorting order of the converse
            # graph. It just so happens that if we reverse the topological sort
            # of a graph, it is a valid topological sorting of the converse
            # graph, so we don't have to compute the converse graph.
            dependency_graph = review_request.build_dependency_graph()
            dependencies = toposort(dependency_graph)[1:]

            if dependencies:
                print(
                    'Recursively landing dependencies of review request %s.' %
                    review_request_id)

                for dependency in dependencies:
                    land_error = self.can_land(dependency)

                    if land_error is not None:
                        raise CommandError(
                            'Aborting recursive land of review request %s.\n'
                            'Review request %s cannot be landed: %s' %
                            (review_request_id, dependency.id, land_error))

                for dependency in reversed(dependencies):
                    self.land(self.options.destination_branch, dependency,
                              None, self.options.squash, self.options.edit,
                              self.options.delete_branch, self.options.dry_run)

        self.land(self.options.destination_branch, review_request, branch_name,
                  self.options.squash, self.options.edit,
                  self.options.delete_branch, self.options.dry_run)

        if self.options.push:
            print('Pushing branch "%s" upstream' %
                  self.options.destination_branch)

            if not self.options.dry_run:
                try:
                    self.tool.push_upstream(self.options.destination_branch)
                except PushError as e:
                    raise CommandError(six.text_type(e))

    def _ask_review_request_match(self, review_request):
        return confirm('Land Review Request #%s: "%s"? ' %
                       (review_request.id,
                        get_draft_or_current_value('summary', review_request)))
Esempio n. 24
0
class Diff(Command):
    """Prints a diff to the terminal."""
    name = "diff"
    author = "The Review Board Project"
    args = "[changenum]"
    option_list = [
        Option("--server",
               dest="server",
               metavar="SERVER",
               config_key="REVIEWBOARD_URL",
               default=None,
               help="specify a different Review Board server to use"),
        Option("--revision-range",
               dest="revision_range",
               default=None,
               help="generate the diff for review based on given "
               "revision range"),
        Option("--parent",
               dest="parent_branch",
               metavar="PARENT_BRANCH",
               help="the parent branch this diff should be against "
               "(only available if your repository supports "
               "parent diffs)"),
        Option("--tracking-branch",
               dest="tracking",
               metavar="TRACKING",
               help="Tracking branch from which your branch is derived "
               "(git only, defaults to origin/master)"),
        Option('--svn-changelist',
               dest='svn_changelist',
               default=None,
               help='generate the diff for review based on a local SVN '
               'changelist'),
        Option("--repository-url",
               dest="repository_url",
               help="the url for a repository for creating a diff "
               "outside of a working copy (currently only "
               "supported by Subversion with --revision-range or "
               "--diff-filename and ClearCase with relative "
               "paths outside the view). For git, this specifies"
               "the origin url of the current repository, "
               "overriding the origin url supplied by the git "
               "client."),
        Option("-d",
               "--debug",
               action="store_true",
               dest="debug",
               config_key="DEBUG",
               default=False,
               help="display debug output"),
        Option("--username",
               dest="username",
               metavar="USERNAME",
               config_key="USERNAME",
               default=None,
               help="user name to be supplied to the Review Board server"),
        Option("--password",
               dest="password",
               metavar="PASSWORD",
               config_key="PASSWORD",
               default=None,
               help="password to be supplied to the Review Board server"),
    ]

    def get_diff(self, *args):
        """Returns a diff as a string."""
        repository_info, tool = self.initialize_scm_tool()
        server_url = self.get_server_url(repository_info, tool)
        root_resource = self.get_root(server_url)
        self.setup_tool(tool, api_root=root_resource)

        if self.options.revision_range:
            diff, parent_diff = tool.diff_between_revisions(
                self.options.revision_range, args, repository_info)
        elif self.options.svn_changelist:
            diff, parent_diff = tool.diff_changelist(
                self.options.svn_changelist)
        else:
            diff, parent_diff = tool.diff(list(args))

        return diff

    def main(self, *args):
        """Print the diff to terminal."""
        diff = self.get_diff(*args)

        if not diff:
            die("There don't seem to be any diffs!")

        print diff
Esempio n. 25
0
import glob
import os
import pkg_resources
import signal
import subprocess
import sys

from rbtools import get_version_string
from rbtools.commands import find_entry_point_for_command, Option, RB_MAIN
from rbtools.utils.aliases import run_alias
from rbtools.utils.filesystem import load_config

GLOBAL_OPTIONS = [
    Option('-v',
           '--version',
           action='version',
           version='RBTools %s (Python %d.%d.%d)' %
           (get_version_string(), sys.version_info[:3][0],
            sys.version_info[:3][1], sys.version_info[:3][2])),
    Option('-h', '--help', action='store_true', dest='help', default=False),
    Option('command',
           nargs=argparse.REMAINDER,
           help='The RBTools command to execute, and any arguments. '
           '(See below)'),
]


def build_help_text(command_class):
    """Generate help text from a command class."""
    command = command_class()
    parser = command.create_parser({})