| """Compilation of utility functions for working with file diffs.""" |
| |
| from __future__ import absolute_import |
| import re |
| |
| import six |
| |
| def diff_to_comments(diff): |
| r"""Converts a unified diff to a map of comments to be added to a review. |
| |
| Comments are formatted according to: |
| https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-input |
| |
| Example output: |
| { |
| 'path/to/foo': [{'line': '1', 'message': '+ bar'}, |
| {'line': '10', 'message': '+ baz\n- foo'}] |
| 'path/to/bar': [{'line': '8', 'message': '- biz'}] |
| } |
| |
| Args: |
| diff: output from difflib.unified_diff() |
| |
| Returns: |
| A dictionary of comments to be added to a review. |
| Keys are filenames and values are lists of dictionaries |
| with 'line' and 'message' entries. |
| """ |
| comments = {} |
| tofile = '' |
| line_range = () |
| message_lines = [] |
| |
| for line in diff: |
| line = line.strip() |
| if line.startswith('+++'): |
| if message_lines: |
| # We transitioned from one file's diffs to the next, |
| # so add last comment from the previous file |
| _add_comment(tofile, line_range, message_lines, comments) |
| message_lines = [] |
| tofile = _get_filename(line) |
| elif line.startswith('---'): |
| # fromfile name is not used |
| continue |
| elif line.startswith('@@') and line.endswith('@@'): |
| if message_lines: |
| _add_comment(tofile, line_range, message_lines, comments) |
| message_lines = [] |
| line_range = _get_linerange(line) |
| else: |
| message_lines.append(line) |
| |
| if message_lines: |
| _add_comment(tofile, line_range, message_lines, comments) |
| |
| return comments |
| |
| |
| def _get_filename(line): |
| """Returns the filename from a unified diff line. |
| |
| The line should be of the format: |
| --- filename\tdate\tdate |
| +++ filename\tdate\tdate |
| |
| Args: |
| line: A line from a unified diff output |
| |
| Returns: |
| the filename in the line, if it exists. '' otherwise. |
| """ |
| assert isinstance(line, six.string_types) |
| assert line.startswith('---') or line.startswith('+++') |
| |
| if len(line) > 4: |
| return line[4:].split('\t', 1)[0] |
| else: |
| return '' |
| |
| |
| def _get_linerange(line): |
| """Returns a tuple of (start_line, end_line) for this change. |
| |
| The line should be of the format: |
| @@ -#,# +#,# @@ |
| |
| Args: |
| line: A line from a unified diff output |
| |
| Returns: |
| The line number of the tofile that the change starts at. |
| """ |
| assert isinstance(line, six.string_types) |
| assert line.startswith('@@') |
| assert line.endswith('@@') |
| |
| matches = re.search(r'-(\d+)(,(\d+))? \+(\d+)(,(\d+))?', line) |
| start_line = matches.group(4) |
| diff_length = matches.group(6) or 1 |
| end_line = str(int(start_line) + int(diff_length) - 1) |
| return (start_line, end_line) |
| |
| |
| def _add_comment(filename, line_range, message_lines, comments): |
| """Add a comment to the list of comments. |
| |
| Args: |
| filename: the file the comment applies to |
| line_range: the (start_line, end_line) numbers that this comment applies to |
| message_lines: a list of lines to put in the comment |
| comments: the dictionary of comments to add this comment to |
| """ |
| assert message_lines |
| assert len(line_range) == 2 |
| |
| start_line, end_line = line_range |
| start_character = 0 |
| end_character = max(len(message_lines[-1]) - 1, 0) |
| # If the message line is a diff'd line, it will have an extra character |
| # at the start that the actual file line does not have. |
| if message_lines[-1].startswith(('-', '+')): |
| end_character -= 1 |
| # Prepend a space to each line so gerrit doesn't format the "-" as a list. |
| message_lines = [' ' + line for line in message_lines] |
| comment = {'range': {'start_line': str(start_line), |
| 'start_character': str(start_character), |
| 'end_line': str(end_line), |
| 'end_character': str(end_character)}, |
| 'message': '\n'.join(message_lines)} |
| if filename in comments: |
| comments[filename].append(comment) |
| else: |
| comments[filename] = [comment] |