1from __future__ import unicode_literals 2 3import logging 4import re 5 6from rbtools.api.errors import APIError 7from rbtools.clients.errors import InvalidRevisionSpecError 8from rbtools.utils.match_score import Score 9from rbtools.utils.repository import get_repository_id 10from rbtools.utils.users import get_user 11 12 13def get_draft_or_current_value(field_name, review_request): 14 """Returns the draft or current field value from a review request. 15 16 If a draft exists for the supplied review request, return the draft's 17 field value for the supplied field name, otherwise return the review 18 request's field value for the supplied field name. 19 """ 20 if review_request.draft: 21 fields = review_request.draft[0] 22 else: 23 fields = review_request 24 25 return fields[field_name] 26 27 28def get_possible_matches(review_requests, summary, description, limit=5): 29 """Returns a sorted list of tuples of score and review request. 30 31 Each review request is given a score based on the summary and 32 description provided. The result is a sorted list of tuples containing 33 the score and the corresponding review request, sorted by the highest 34 scoring review request first. 35 """ 36 candidates = [] 37 38 # Get all potential matches. 39 for review_request in review_requests.all_items: 40 summary_pair = (get_draft_or_current_value('summary', review_request), 41 summary) 42 description_pair = (get_draft_or_current_value('description', 43 review_request), 44 description) 45 score = Score.get_match(summary_pair, description_pair) 46 candidates.append((score, review_request)) 47 48 # Sort by summary and description on descending rank. 49 sorted_candidates = sorted( 50 candidates, 51 key=lambda m: (m[0].summary_score, m[0].description_score), 52 reverse=True 53 ) 54 55 return sorted_candidates[:limit] 56 57 58def get_revisions(tool, cmd_args): 59 """Returns the parsed revisions from the command line arguments. 60 61 These revisions are used for diff generation and commit message 62 extraction. They will be cached for future calls. 63 """ 64 # Parse the provided revisions from the command line and generate 65 # a spec or set of specialized extra arguments that the SCMClient 66 # can use for diffing and commit lookups. 67 try: 68 revisions = tool.parse_revision_spec(cmd_args) 69 except InvalidRevisionSpecError: 70 if not tool.supports_diff_extra_args: 71 raise 72 73 revisions = None 74 75 return revisions 76 77 78def find_review_request_by_change_id(api_client, api_root, repository_info, 79 repository_name, revisions): 80 """Ask ReviewBoard for the review request ID for the tip revision. 81 82 Note that this function calls the ReviewBoard API with the only_fields 83 paramater, thus the returned review request will contain only the fields 84 specified by the only_fields variable. 85 86 If no review request is found, None will be returned instead. 87 """ 88 only_fields = 'id,commit_id,changenum,status,url,absolute_url' 89 change_id = revisions['tip'] 90 logging.debug('Attempting to find review request from tip revision ID: %s', 91 change_id) 92 # Strip off any prefix that might have been added by the SCM. 93 change_id = change_id.split(':', 1)[1] 94 95 optional_args = {} 96 97 if change_id.isdigit(): 98 # Populate integer-only changenum field also for compatibility 99 # with older API versions 100 optional_args['changenum'] = int(change_id) 101 102 user = get_user(api_client, api_root, auth_required=True) 103 repository_id = get_repository_id( 104 repository_info, api_root, repository_name) 105 106 # Don't limit query to only pending requests because it's okay to stamp a 107 # submitted review. 108 review_requests = api_root.get_review_requests(repository=repository_id, 109 from_user=user.username, 110 commit_id=change_id, 111 only_links='self', 112 only_fields=only_fields, 113 **optional_args) 114 115 if review_requests: 116 count = review_requests.total_results 117 118 # Only one review can be associated with a specific commit ID. 119 if count > 0: 120 assert count == 1, '%d review requests were returned' % count 121 review_request = review_requests[0] 122 logging.debug('Found review request %s with status %s', 123 review_request.id, review_request.status) 124 125 if review_request.status != 'discarded': 126 return review_request 127 128 return None 129 130 131def guess_existing_review_request(repository_info, repository_name, 132 api_root, api_client, tool, revisions, 133 guess_summary, guess_description, 134 is_fuzzy_match_func=None, 135 no_commit_error=None, 136 submit_as=None, additional_fields=None): 137 """Try to guess the existing review request ID if it is available. 138 139 The existing review request is guessed by comparing the existing 140 summary and description to the current post's summary and description, 141 respectively. The current post's summary and description are guessed if 142 they are not provided. 143 144 If the summary and description exactly match those of an existing 145 review request, that request is immediately returned. Otherwise, 146 the user is prompted to select from a list of potential matches, 147 sorted by the highest ranked match first. 148 149 Note that this function calls the ReviewBoard API with the only_fields 150 paramater, thus the returned review request will contain only the fields 151 specified by the only_fields variable. 152 """ 153 only_fields = [ 154 'id', 'summary', 'description', 'draft', 'url', 'absolute_url', 155 'bugs_closed', 'status', 'public' 156 ] 157 158 if additional_fields: 159 only_fields += additional_fields 160 161 if submit_as: 162 username = submit_as 163 else: 164 user = get_user(api_client, api_root, auth_required=True) 165 username = user.username 166 167 repository_id = get_repository_id( 168 repository_info, api_root, repository_name) 169 170 try: 171 # Get only pending requests by the current user for this 172 # repository. 173 review_requests = api_root.get_review_requests( 174 repository=repository_id, 175 from_user=username, 176 status='pending', 177 expand='draft', 178 only_fields=','.join(only_fields), 179 only_links='diffs,draft', 180 show_all_unpublished=True) 181 182 if not review_requests: 183 raise ValueError('No existing review requests to update for ' 184 'user %s' 185 % username) 186 except APIError as e: 187 raise ValueError('Error getting review requests for user %s: %s' 188 % (username, e)) 189 190 summary = None 191 description = None 192 193 if not guess_summary or not guess_description: 194 try: 195 commit_message = tool.get_commit_message(revisions) 196 197 if commit_message: 198 if not guess_summary: 199 summary = commit_message['summary'] 200 201 if not guess_description: 202 description = commit_message['description'] 203 elif callable(no_commit_error): 204 no_commit_error() 205 except NotImplementedError: 206 raise ValueError('--summary and --description are required.') 207 208 if not summary and not description: 209 return None 210 211 possible_matches = get_possible_matches(review_requests, summary, 212 description) 213 exact_match_count = num_exact_matches(possible_matches) 214 215 for score, review_request in possible_matches: 216 # If the score is the only exact match, return the review request 217 # ID without confirmation, otherwise prompt. 218 if ((score.is_exact_match() and exact_match_count == 1) or 219 (callable(is_fuzzy_match_func) and 220 is_fuzzy_match_func(review_request))): 221 return review_request 222 223 return None 224 225 226def num_exact_matches(possible_matches): 227 """Returns the number of exact matches in the possible match list.""" 228 count = 0 229 230 for score, request in possible_matches: 231 if score.is_exact_match(): 232 count += 1 233 234 return count 235 236 237def parse_review_request_url(url): 238 """Parse a review request URL and return its component parts. 239 240 Args: 241 url (unicode): 242 The URL to parse. 243 244 Returns: 245 tuple: 246 A 3-tuple consisting of the server URL, the review request ID, and the 247 diff revision. 248 """ 249 regex = (r'^(?P<server_url>https?:\/\/.*\/(?:\/s\/[^\/]+\/)?)' 250 r'r\/(?P<review_request_id>\d+)' 251 r'\/?(diff\/(?P<diff_id>\d+-?\d*))?\/?') 252 match = re.match(regex, url) 253 254 if match: 255 server_url = match.group('server_url') 256 request_id = match.group('review_request_id') 257 diff_id = match.group('diff_id') 258 return (server_url, request_id, diff_id) 259 260 return (None, None, None) 261