예제 #1
0
파일: stamp.py 프로젝트: dakkar/rbtools
    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)
예제 #2
0
    def download_file(self, url, label=None):
        """Download the given file.

        This is intended to be used as a context manager, and the bound value
        will be the filename of the downloaded file.

        Args:
            url (unicode):
                The URL of the file to download.

            label (unicode, optional):
                The label to use for the progress bar. If this is not
                specified, no progress bar will be shown.

        Yields:
            unicode:
            The filename of the downloaded file.

        Raises:
            rbtools.commands.CommandError:
                An error occurred while downloading the file.
        """
        logging.debug('Downloading %s', url)

        try:
            response = urlopen(url)

            total_bytes = int(
                response.info().getheader('Content-Length').strip())
            read_bytes = 0
            bar_format = '{desc} {bar} {percentage:3.0f}% [{remaining}]'

            with tqdm.tqdm(total=total_bytes,
                           desc=label or '',
                           ncols=80,
                           disable=label is None,
                           bar_format=bar_format) as bar:
                try:
                    f = tempfile.NamedTemporaryFile(delete=False)
                    while read_bytes != total_bytes:
                        chunk = response.read(8192)
                        chunk_length = len(chunk)
                        read_bytes += chunk_length

                        f.write(chunk)

                        bar.update(chunk_length)
                finally:
                    f.close()

            return f.name
        except (HTTPError, URLError) as e:
            raise CommandError('Error when downloading file: %s' % e)
예제 #3
0
    def generate_config_file(self, file_path, config):
        """Generates the config file in the current working directory."""
        try:
            with open(file_path, 'w') as outfile:
                output = self._get_output(config)
                outfile.write(output)
        except IOError as e:
            raise CommandError('I/O error generating config file (%s): %s'
                               % (e.errno, e.strerror))

        print('%s creation successful! Config written to %s' % (CONFIG_FILE,
                                                                file_path))
예제 #4
0
    def main(self, *args):
        """Add the review request URL to a commit message."""
        self.cmd_args = list(args)

        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_id = self.options.rid

            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))

            review_request_url = review_request.absolute_url
        else:
            review_request_id, review_request_url = \
                self. determine_review_request(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)

        self.stdout.write('Successfully stamped change with the URL:')
        self.stdout.write(review_request_url)
예제 #5
0
    def unzip(self, zip_filename, package_dir):
        """Unzip a .zip file.

        This method will unpack the contents of a .zip file into a target
        directory. If that directory already exists, it will first be removed.

        Args:
            zip_filename (unicode):
                The absolute path to the .zip file to unpack.

            package_dir (unicode):
                The directory to unzip the files into.

        Raises:
            rbtools.commands.CommandError:
                The file could not be unzipped.
        """
        logging.debug('Extracting %s to %s', zip_filename, package_dir)

        try:
            if os.path.exists(package_dir):
                if os.path.isdir(package_dir):
                    shutil.rmtree(package_dir)
                else:
                    os.remove(package_dir)

            os.makedirs(package_dir)
        except (IOError, OSError) as e:
            raise CommandError('Failed to set up package directory %s: %s' %
                               (package_dir, e))

        zip_file = zipfile.ZipFile(zip_filename, 'r')

        try:
            zip_file.extractall(package_dir)
        except Exception as e:
            raise CommandError('Failed to extract file: %s' % e)
        finally:
            zip_file.close()
예제 #6
0
파일: publish.py 프로젝트: schemacs/rbtools
    def main(self, request_id):
        """Run the command."""
        repository_info, tool = self.initialize_scm_tool()
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)

        request = self.get_review_request(request_id, api_root)
        try:
            draft = request.get_draft()
            draft = draft.update(public=True)
        except APIError, e:
            raise CommandError("Error publishing review request (it may "
                               "already be published): %s" % e)
예제 #7
0
    def post_process_options(self):
        # -g implies --guess-summary and --guess-description
        if self.options.guess_fields:
            self.options.guess_summary = True
            self.options.guess_description = True

        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 description, override
        # --guess-description
        if self.options.guess_description and self.options.description:
            self.options.guess_description = False

        # If we have an explicitly specified review request ID, override
        # --update
        if self.options.rid and self.options.update:
            self.options.update = False
예제 #8
0
파일: attach.py 프로젝트: schemacs/rbtools
    def main(self, request_id, path_to_file):
        self.repository_info, self.tool = self.initialize_scm_tool()
        server_url = self.get_server_url(self.repository_info, self.tool)
        api_client, api_root = self.get_api(server_url)

        request = self.get_review_request(request_id, api_root)

        try:
            f = open(path_to_file, 'r')
            content = f.read()
            f.close()
        except IOError:
            raise CommandError("%s is not a valid file." % path_to_file)

        # Check if the user specified a custom filename, otherwise
        # use the original filename.
        filename = self.options.filename or os.path.basename(path_to_file)

        try:
            request.get_file_attachments() \
                .upload_attachment(filename, content, self.options.caption)
        except APIError, e:
            raise CommandError("Error uploading file: %s" % e)
예제 #9
0
    def main(self, 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)

        request = get_review_request(request_id, api_root)

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

        print('Review request #%s is published.' % request_id)
예제 #10
0
    def main(self, 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)

        review_request = get_review_request(request_id, api_root,
                                            only_fields='public',
                                            only_links='draft')

        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.' % request_id)
예제 #11
0
파일: land.py 프로젝트: abaelhe/rbtools
    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)
예제 #12
0
    def main(self, action):
        """Call API for status-update.

        Args:
            action (unicode):
                Sub command argument input for specifying which action to do
                (can be ``get``, ``set``, or ``delete``).

        Raises:
            rbtools.command.CommandError:
                Error with the execution of the command.
        """
        if action == 'get':
            self.get_status_update(api_root)
        elif action == 'set':
            self.set_status_update(api_root)
        elif action == 'delete':
            self.delete_status_update(api_root)
        else:
            raise CommandError('Action "%s" not recognized.' % action)
예제 #13
0
    def main(self, request_id):
        """Run the command."""
        close_type = self.options.close_type
        self.check_valid_type(close_type)
        repository_info, tool = self.initialize_scm_tool()
        server_url = self.get_server_url(repository_info, tool)
        api_client, api_root = self.get_api(server_url)
        request = self.get_review_request(request_id, api_root)

        if request.status == close_type:
            raise CommandError("Request request #%s is already %s." %
                               (request_id, close_type))

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

        print "Review request #%s is set to %s." % (request_id, request.status)
예제 #14
0
    def apply_patch(self, repository_info, tool, request_id, diff_revision,
                    diff_file_path, base_dir):
        """Apply patch patch_file and display results to user."""
        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)

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

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

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

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

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

            return False
        else:
            print 'Successfully applied patch.'

            return True
예제 #15
0
    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))
예제 #16
0
    def main(self):
        """Run the command."""
        session = self.api_root.get_session(expand='user')
        was_authenticated = session.authenticated

        if not was_authenticated:
            try:
                session = get_authenticated_session(api_client=self.api_client,
                                                    api_root=self.api_root,
                                                    auth_required=True,
                                                    session=session)
            except AuthorizationError:
                raise CommandError('Unable to log in to Review Board.')

        if session.authenticated:
            if not was_authenticated or (self.options.username
                                         and self.options.password):
                logging.info('Successfully logged in to Review Board.')
            else:
                logging.info('You are already logged in to Review Board at %s',
                             self.api_client.domain)
예제 #17
0
    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
예제 #18
0
    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')

        try:
            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)
        except ValueError as e:
            raise CommandError(six.text_type(e))

        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
예제 #19
0
파일: post.py 프로젝트: vlovich/rbtools
    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!")

        try:
            diff_validator = api_root.get_validation().get_diff_validation()
            diff_validator.validate_diff(repository,
                                         diff,
                                         parent_diff=parent_diff,
                                         base_dir=base_dir)
        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)
예제 #20
0
    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)

        if self.options.revision_range:
            raise CommandError(
                'The --revision-range argument has been removed. To create 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.')

        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)

        try:
            revisions = tool.parse_revision_spec(args)
            extra_args = None
        except InvalidRevisionSpecError:
            if not tool.supports_diff_extra_args:
                raise

            revisions = None
            extra_args = args

        if (self.options.exclude_patterns
                and not 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.' % tool.name)

        diff_kwargs = {}

        if self.options.no_renames:
            if not tool.supports_no_renames:
                raise CommandError(
                    'The %s SCM tool does not support diffs '
                    'without renames.', tool.type)

            diff_kwargs['no_renames'] = True

        if self.options.git_find_renames_threshold is not None:
            diff_kwargs['git_find_renames_threshold'] = \
                self.options.git_find_renames_threshold

        diff_info = tool.diff(revisions=revisions,
                              include_files=self.options.include_files or [],
                              exclude_patterns=self.options.exclude_patterns
                              or [],
                              extra_args=extra_args,
                              **diff_kwargs)

        diff = diff_info['diff']

        if diff:
            if six.PY2:
                print(diff)
            else:
                # Write the non-decoded binary diff to standard out
                sys.stdout.buffer.write(diff)
                print()
예제 #21
0
 def no_commit_error(self):
     raise CommandError('No existing commit to stamp on.')
예제 #22
0
                if submit_as:
                    request_data['submit_as'] = submit_as

                review_request = api_root.get_review_requests().create(
                    **request_data)
            except APIError, e:
                if e.error_code == 204:  # Change number in use.
                    rid = e.rsp['review_request']['id']
                    review_request = api_root.get_review_request(
                        review_request_id=rid)

                    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:
                basedir = (self.options.basedir or repository_info.base_path)
                review_request.get_diffs().upload_diff(
                    diff_content,
                    parent_diff=parent_diff_content,
                    base_dir=basedir)
            except APIError, e:
                error_msg = [
                    'Error uploading diff\n\n',
                ]

                if e.error_code == 101 and e.http_status == 403:
예제 #23
0
def guess_existing_review_request(repository_info,
                                  repository_name,
                                  api_root,
                                  api_client,
                                  tool,
                                  revisions,
                                  guess_summary,
                                  guess_description,
                                  is_fuzzy_match_func=None,
                                  no_commit_error=None,
                                  submit_as=None):
    """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, that request is immediately returned. Otherwise,
    the user is prompted to select from a list of potential matches,
    sorted by the highest ranked match first.

    Note that this function calls the ReviewBoard API with the only_fields
    paramater, thus the returned review request will contain only the fields
    specified by the only_fields variable.
    """
    only_fields = 'id,summary,description,draft,url,absolute_url'

    if submit_as:
        username = submit_as
    else:
        user = get_user(api_client, api_root, auth_required=True)
        username = user.username

    repository_id = get_repository_id(repository_info, api_root,
                                      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=username,
            status='pending',
            expand='draft',
            only_fields=only_fields,
            only_links='draft',
            show_all_unpublished=True)

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

    summary = None
    description = None

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

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

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

    if not summary and not description:
        return None

    possible_matches = get_possible_matches(review_requests, summary,
                                            description)
    exact_match_count = 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)
                or (callable(is_fuzzy_match_func)
                    and is_fuzzy_match_func(review_request))):
            return review_request

    return None
예제 #24
0
파일: post.py 프로젝트: vlovich/rbtools
    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)

            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_request = api_root.get_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)

                    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().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()
        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
예제 #25
0
파일: land.py 프로젝트: abaelhe/rbtools
    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))
예제 #26
0
파일: post.py 프로젝트: hunz-dev/rbtools
    def post_process_options(self):
        super(Post, self).post_process_options()

        # -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.')

        # 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.' %
                                   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.')

        # 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.' %
                                   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 the --diff-filename argument is used, we can't do automatic
        # updating.
        if self.options.diff_filename and self.options.update:
            raise CommandError('The --update option cannot be used when '
                               'using --diff-filename.')

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

        if self.options.trivial_publish:
            self.options.publish = True
예제 #27
0
    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.
        """
        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(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:
            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))
예제 #28
0
 def get_review_request(self, request_id, api_root):
     """Returns the review request resource for the given ID."""
     try:
         request = api_root.get_review_request(review_request_id=request_id)
     except APIError, e:
         raise CommandError("Error getting review request: %s" % e)
예제 #29
0
파일: post.py 프로젝트: hunz-dev/rbtools
    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,public',
                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:
                # Until we are Python 2.7+ only, the keys in request_data have
                # to be bytes. See bug 3753 for details.
                request_data = {b'repository': repository}

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

                if submit_as:
                    request_data[b'submit_as'] = submit_as

                if self.tool.can_bookmark:
                    bookmark = self.tool.get_current_bookmark()
                    request_data[b'extra_data__local_bookmark'] = bookmark
                elif self.tool.can_branch:
                    branch = self.tool.get_current_branch()
                    request_data[b'extra_data__local_branch'] = branch

                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:
                    # The change number is already in use. Get the review
                    # request for that change and update it instead.
                    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')
                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)

        # Stamp the commit message with the review request URL before posting
        # the review, so that we can use the stamped commit message when
        # guessing the description. This enables the stamped message to be
        # present on the review if the user has chosen to publish immediately
        # upon posting.
        if self.options.stamp_when_posting:
            if not self.tool.can_amend_commit:
                print('Cannot stamp review URL onto the commit message; '
                      'stamping is not supported with %s.' % self.tool.name)

            else:
                try:
                    stamp_commit_with_review_url(self.revisions,
                                                 review_request.absolute_url,
                                                 self.tool)
                    print('Stamped review URL onto the commit message.')
                except AlreadyStampedError:
                    print('Commit message has already been stamped')
                except Exception as e:
                    logging.debug(
                        'Caught exception while stamping the '
                        'commit message. Proceeding to post '
                        'without stamping.',
                        exc_info=True)
                    print('Could not stamp review URL onto the commit '
                          'message.')

        # If the user has requested to guess the summary or description,
        # get the commit message and override the summary and description
        # options. The guessing takes place after stamping so that the
        # guessed description matches the commit when rbt exits.
        if not self.options.diff_filename:
            self.check_guess_fields()

        # 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.publish:
            update_fields['public'] = True

            if (self.options.trivial_publish
                    and self.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 self.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. Use --description instead.')

        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(
                    'Error updating review request draft: %s\n\n'
                    'Your review request still exists, but the diff is not '
                    'attached.\n\n'
                    '%s\n' % (e, review_request.absolute_url))

        return review_request.id, review_request.absolute_url
예제 #30
0
파일: post.py 프로젝트: hunz-dev/rbtools
    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 repository is None:
            raise CommandError('Could not find the repository on the Review '
                               'Board server.')

        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 not self.options.diff_filename
                and 'changenum' in diff_info):
            changenum = diff_info['changenum']
        else:
            changenum = self.tool.get_changenum(self.revisions)

        # Not all scm clients support get_changenum, so if get_changenum
        # returns None (the default for clients that don't have changenums),
        # we'll prefer the existing commit_id.
        commit_id = changenum or commit_id

        if self.options.update and self.revisions:
            review_request = guess_existing_review_request(
                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,
                submit_as=self.options.submit_as)

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

            self.options.rid = review_request.id

        # If only certain files within a commit are being submitted for review,
        # do not include the commit id. This prevents conflicts if multiple
        # files from the same commit are posted for review separately.
        if self.options.include_files or self.options.exclude_patterns:
            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)