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)
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)
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))
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)
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()
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)
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
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)
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)
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)
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 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)
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)
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
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): """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)
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 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
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)
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()
def no_commit_error(self): raise CommandError('No existing commit to stamp on.')
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:
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
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
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 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
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))
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)
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
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)