blob: fccd46c42cf8a661da9c2f45ed7e6d5591222a97 [file] [log] [blame] [edit]
"""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]