← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjohnston/launchpad/short-ic-emails into lp:launchpad

 


Diff comments:

> === modified file 'lib/lp/code/mail/codereviewcomment.py'
> --- lib/lp/code/mail/codereviewcomment.py	2014-05-21 08:52:53 +0000
> +++ lib/lp/code/mail/codereviewcomment.py	2014-07-26 02:00:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Email notifications for code review comments."""
> @@ -26,6 +26,7 @@
>      append_footer,
>      format_address,
>      )
> +from lp.services.patches import parse_patches
>  from lp.services.webapp import canonical_url
>  
>  
> @@ -171,16 +172,51 @@
>  
>  def build_inline_comments_section(comments, diff_text):
>      """Return a formatted text section with contextualized comments."""
> -    result_lines = []
> -    diff_lines = diff_text.splitlines()
> -    for num, line in enumerate(diff_lines, 1):
> -        result_lines.append(u'> {0}'.format(line.decode('utf-8', 'replace')))
> -        comment = comments.get(str(num))
> -        if comment is not None:
> -            result_lines.append('')
> -            result_lines.extend(comment.splitlines())
> -            result_lines.append('')
> -
> -    result_text = u'\n'.join(result_lines)
> +    result_diff = []
> +    lines = diff_text.splitlines(True)
> +    patches = parse_patches(lines, allow_dirty=True)
> +    line_count = 0
> +    for patch in patches:
> +        patch_code = []
> +        patch_has_ic = False
> +        headers = patch.get_header()
> +        has_header_ic = False
> +        header_code = []
> +        for line in headers.splitlines():
> +            line_count += 1
> +            comment = comments.get(str(line_count))
> +            header_code.append(u'> {0}'.format(
> +                line.decode('utf-8', 'replace')))
> +            if comment is not None:
> +                header_code.append('')
> +                header_code.extend(comment.splitlines())
> +                header_code.append('')
> +                has_header_ic = True
> +
> +        for p in patch.hunks:
> +            hunk_code = []
> +            has_ic = False

As we discussed, using has_ic and patch_has_ic is the safest way to do this at the present time.

> +            diff_code = str(p).splitlines()
> +            for num, line in enumerate(diff_code, 1):
> +                line_count += 1
> +                hunk_code.append(u'> {0}'.format(
> +                    line.decode('utf-8', 'replace')))
> +                comment = comments.get(str(line_count))
> +                if comment is not None:
> +                    hunk_code.append('')
> +                    hunk_code.extend(comment.splitlines())
> +                    hunk_code.append('')
> +                    has_ic = True
> +            if has_ic:
> +                patch_code += hunk_code
> +                patch_has_ic = True
> +            else:
> +                if (len(patch_code) > 0 and
> +                        patch_code[-1] != u'') or len(patch_code) == 0:
> +                    patch_code.append('')
> +        if has_header_ic or patch_has_ic:
> +            result_diff += header_code + patch_code
> +
> +    result_text = u'\n'.join(result_diff)
>  
>      return '\n\nDiff comments:\n\n%s\n\n' % result_text
> 
> === modified file 'lib/lp/code/mail/tests/test_codereviewcomment.py'
> --- lib/lp/code/mail/tests/test_codereviewcomment.py	2014-05-21 09:33:04 +0000
> +++ lib/lp/code/mail/tests/test_codereviewcomment.py	2014-07-26 02:00:47 +0000
> @@ -1,8 +1,9 @@
> -# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Test CodeReviewComment emailing functionality."""
>  
> +import os
>  
>  import testtools
>  import transaction
> @@ -31,6 +32,12 @@
>  from lp.testing.layers import LaunchpadFunctionalLayer
>  
>  
> +def datafile(filename):
> +    data_path = os.path.join(os.path.dirname(__file__),
> +                             "../../../services/tests/data", filename)
> +    return file(data_path, "rb")
> +
> +
>  class TestCodeReviewComment(TestCaseWithFactory):
>      """Test that comments are generated as expected."""
>  
> @@ -215,13 +222,14 @@
>                           mailer.message.text_contents.splitlines())
>  
>      def makeCommentWithInlineComments(self, subject=None, content=None,
> -                                      inline_comments=None):
> +                                      inline_comments=None, diff_text=None):
>          """Create a `CodeReviewComment` with inline (diff) comments."""
>          bmp = self.factory.makeBranchMergeProposal()
>          bmp.source_branch.subscribe(bmp.registrant,
>              BranchSubscriptionNotificationLevel.NOEMAIL, None,
>              CodeReviewNotificationLevel.FULL, bmp.registrant)
> -        previewdiff = self.factory.makePreviewDiff(merge_proposal=bmp)
> +        previewdiff = self.factory.makePreviewDiff(merge_proposal=bmp,
> +                                                   diff_text=diff_text)
>          transaction.commit()
>          if subject is None:
>              subject = 'A comment'
> @@ -235,14 +243,184 @@
>              inline_comments=inline_comments)
>          return comment
>  
> -    def test_generateEmailWithInlineComments(self):
> -        """Review comments emails consider the inline comments.
> -
> -        See `build_inline_comments_section` tests for formatting details.
> -        """
> -        # Enabled corresponding feature flag.
> -        self.useContext(feature_flags())
> -        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +    # For the test_inline_comments_email_* tests, please see
> +    # `build_inline_comments_section` tests for formatting details.
> +
> +    def test_inline_comments_email_text(self):
> +        # Testing that a single file with three hunks is parsed properly and
> +        # only the header/hunk combinations with comments are provided.
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff = datafile("diff-1").read()
> +        expected = datafile("expected-1").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'9': u'Cool',
> +                             '27': u'Are you sure?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_binary(self):
> +        # Testing that a single file with three hunks and a binary file is
> +        # parsed properly and only the binary header with comments are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff = u'\n'.join([diff_1, diff_2])
> +        expected = datafile("expected-2").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'33': u'Why are you adding this?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_binary(self):
> +        # Testing that a binary file is parsed properly and only the binary
> +        # header with comments are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff = datafile("diff-2").read()
> +        expected = datafile("expected-2").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'2': u'Why are you adding this?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_binary_text(self):
> +        # Testing that a single file with three hunks, a binary file and a
> +        # text file is parsed properly and only the binary header with comments
> +        # are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff_3 = datafile("diff-3").read()
> +        diff = u'\n'.join([diff_1, diff_2, diff_3])
> +        expected = datafile("expected-3").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'43': u'What is this?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_binary_text_includes_two_files(self):
> +        # Testing that a single file with three hunks, a binary file and a
> +        # text file is parsed properly and a single hunk from the first file
> +        # plus a single hunk from the second file with comments are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff_3 = datafile("diff-3").read()
> +        diff = u'\n'.join([diff_1, diff_2, diff_3])
> +        expected = datafile("expected-5").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'18': u'Why?',
> +                             '43': u'Good catch!'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_binary_text_includes_three_files(self):
> +        # Testing that a single file with three hunks, a binary file and a
> +        # text file is parsed properly and two hunks from the first file,
> +        # the second file and a single hunk from the second file with comments
> +        # are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff_3 = datafile("diff-3").read()
> +        diff = u'\n'.join([diff_1, diff_2, diff_3])
> +        expected = datafile("expected-6").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'27': u'+1',

This is dropped in order to test that we only add one blank line (skipped line identifier) even when two hunks are skipped.

> +                             '33': u'Do we need this?',
> +                             '43': u'Can we change this?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_binary_text_no_nl_files(self):
> +        # Testing that a single file with three hunks, a binary file and a
> +        # text file which do not have a new line between each other is parsed
> +        # properly and two hunks from the first file, the second file and a
> +        # single hunk from the second file with comments are provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff_3 = datafile("diff-3").read()
> +        diff = u''.join([diff_1, diff_2, diff_3])
> +        expected = datafile("expected-7").read().decode().splitlines()
> +
> +        comment = self.makeCommentWithInlineComments(
> +            inline_comments={'9': u'Good stuff',
> +                             '27': u'+1',
> +                             '32': u'Do we need this?',
> +                             '41': u'Can we change this?'},
> +            diff_text=diff)
> +        mailer = CodeReviewCommentMailer.forCreation(comment)
> +        commenter = comment.branch_merge_proposal.registrant
> +        ctrl = mailer.generateEmail(
> +            commenter.preferredemail.email, commenter)
> +
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
> +
> +    def test_inline_comments_email_text_comment_in_header(self):
> +        # Testing that a single file with comments in the header is parsed
> +        # properly and only the headers is provided
> +
> +        # Enabled corresponding feature flag.
> +        self.useContext(feature_flags())
> +        set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
> +        expected = datafile("expected-4").read().decode('utf-8').splitlines()
>  
>          comment = self.makeCommentWithInlineComments(
>              inline_comments={'2': u'Is this from Planet Earth\xa9 ?'})
> @@ -251,20 +429,7 @@
>          ctrl = mailer.generateEmail(
>              commenter.preferredemail.email, commenter)
>  
> -        expected_lines = [
> -            '',
> -            'Diff comments:',
> -            '',
> -            "> === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'",
> -            '> --- yvo/yc/pbqr/vagresnprf/qvss.cl      '
> -            '2009-10-01 13:25:12 +0000',
> -            '',
> -            u'Is this from Planet Earth\xa9 ?',
> -            '',
> -            '> +++ yvo/yc/pbqr/vagresnprf/qvss.cl      '
> -            '2010-02-02 15:48:56 +0000'
> -        ]
> -        self.assertEqual(expected_lines, ctrl.body.splitlines()[1:10])
> +        self.assertEqual(expected, ctrl.body.splitlines()[:-2])
>  
>      def test_generateEmailWithInlineComments_feature_disabled(self):
>          """Inline comments are not considered if the flag is not enabled."""
> 
> === added file 'lib/lp/services/patches.py'
> --- lib/lp/services/patches.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/patches.py	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,235 @@
> +# Copyright 2014 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +# This file is derived from bzrlib.patches in order to extend some
> +# functionality for use by the Launchpad inline comment feature.
> +
> +# In order to reduce the size of a diff being emailed with inline comments
> +# to just the relevant hunks of code, it was needed to break the diff down
> +# and then put the required pieces back together. To do this, the notion of
> +# 'comments' was added to Patch and BinaryPatch in order to return any lines
> +# beginning with '===' which is later added to the file headers and the line
> +# counted.
> +
> +import re
> +
> +from bzrlib.errors import (
> +    BzrError,
> +    MalformedHunkHeader,
> +    MalformedPatchHeader,
> +    PatchSyntax,
> +    )
> +from bzrlib.patches import (
> +    binary_files_re,
> +    ContextLine,
> +    hunk_from_header,
> +    InsertLine,
> +    iter_lines_handle_nl,
> +    parse_line,
> +    RemoveLine,
> +    )
> +
> +
> +regex = re.compile(binary_files_re)
> +
> +
> +class BinaryFiles(BzrError):
> +
> +    _fmt = 'Binary files section encountered.'
> +
> +    def __init__(self, comments, line):
> +        self.comments = comments
> +        self.line = line
> +
> +
> +class BinaryPatch(object):
> +    def __init__(self, comments, line):
> +        self.comments = comments
> +        self.line = line
> +        self.hunks = []
> +
> +    def __str__(self):
> +        return self.get_header()
> +
> +    def get_header(self):
> +        comments = ''.join(self.comments + [self.line])
> +        return "%s" % comments
> +
> +
> +class Patch(object):
> +    def __init__(self, comments, oldname, newname):
> +        self.oldname = oldname
> +        self.newname = newname
> +        self.hunks = []
> +        self.comments = comments
> +
> +    def __str__(self):
> +        ret = self.get_header()
> +        ret += "".join([str(h) for h in self.hunks])
> +        return ret
> +
> +    def get_header(self):
> +        comments = ''.join(self.comments)
> +        return "%s--- %s\n+++ %s\n" % (comments, self.oldname, self.newname)
> +
> +
> +def get_patch_names(iter_lines):
> +    comments = []
> +    line = iter_lines.next()
> +    while ((line == "\n") or line.startswith('=== ')
> +            or line.startswith('*** ') or line.startswith('#')):
> +        comments.append(line)
> +        line = iter_lines.next()
> +    try:
> +        match = re.match(binary_files_re, line)
> +        if match is not None:
> +            match_line = line
> +            try:
> +                keeper = line
> +                next_line = iter_lines.next()
> +                if next_line == '\n':
> +                    match_line += next_line
> +                    line = match_line
> +                else:
> +                    line = keeper
> +            except:
> +                pass
> +            raise BinaryFiles(comments, match_line)
> +        if not line.startswith("--- "):
> +            raise MalformedPatchHeader("No orig name", line)
> +        else:
> +            orig_name = line[4:].rstrip("\n")
> +    except StopIteration:
> +        raise MalformedPatchHeader("No orig line", "")
> +    try:
> +        line = iter_lines.next()
> +        if not line.startswith("+++ "):
> +            raise PatchSyntax("No mod name")
> +        else:
> +            mod_name = line[4:].rstrip("\n")
> +    except StopIteration:
> +        raise MalformedPatchHeader("No mod line", "")
> +    return (comments, orig_name, mod_name)
> +
> +
> +def iter_hunks(iter_lines, allow_dirty=False):
> +    '''
> +    :arg iter_lines: iterable of lines to parse for hunks
> +    :kwarg allow_dirty: If True, when we encounter something that is not
> +        a hunk header when we're looking for one, assume the rest of the lines
> +        are not part of the patch (comments or other junk).  Default False
> +    '''
> +    hunk = None
> +    line_no = 0
> +    for line in iter_lines:
> +        line_no += 1  # 1 based index

Copied straight from bzrlib.patches... removed.

> +        if line == "\n":
> +            if hunk is not None:
> +                hunk.lines.append(line)
> +                yield hunk
> +                hunk = None
> +            continue
> +        if hunk is not None:
> +            yield hunk
> +        try:
> +            hunk = hunk_from_header(line)
> +        except MalformedHunkHeader:
> +            if allow_dirty:
> +                # If the line isn't a hunk header, then we've reached the end
> +                # of this patch and there's "junk" at the end.  Ignore the
> +                # rest of this patch.
> +                return
> +            raise
> +        orig_size = 0
> +        mod_size = 0
> +        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
> +            hunk_line = parse_line(iter_lines.next())
> +            hunk.lines.append(hunk_line)
> +            if isinstance(hunk_line, (RemoveLine, ContextLine)):
> +                orig_size += 1
> +            if isinstance(hunk_line, (InsertLine, ContextLine)):
> +                mod_size += 1
> +    if hunk is not None:
> +        yield hunk
> +
> +
> +def iter_file_patch(iter_lines, allow_dirty=False):
> +    '''
> +    :arg iter_lines: iterable of lines to parse for patches
> +    :kwarg allow_dirty: If True, allow comments and other non-patch text
> +        before the first patch.  Note that the algorithm here can only find
> +        such text before any patches have been found.  Comments after the
> +        first patch are stripped away in iter_hunks() if it is also passed
> +        allow_dirty=True.  Default False.
> +    '''
> +    ### FIXME: Docstring is not quite true.  We allow certain comments no
> +    # matter what, If they startwith '===', '***', or '#' Someone should
> +    # reexamine this logic and decide if we should include those in
> +    # allow_dirty or restrict those to only being before the patch is found
> +    # (as allow_dirty does).
> +    saved_lines = []
> +    keeper = []
> +    binary = False
> +    for line in iter_lines:
> +        # if it's a === or binary it's a new patch, but we might have
> +        # both together.
> +        if line.startswith('\n') and binary:
> +            saved_lines.append(line)
> +            binary = False
> +        elif binary:
> +            binary = False
> +        if line.startswith('=== '):
> +            # assume a new patch until we know differently
> +            if len(saved_lines) > 0:
> +                yield saved_lines
> +                saved_lines = []
> +            keeper.append(line)
> +        elif regex.match(line):
> +            # check for preceeding '=== ' line
> +            # a binary patch always yields the previous patch
> +            if len(saved_lines) > 0:
> +                saved_lines.append(line)
> +                binary = True
> +            else:
> +                yield [line]
> +        else:
> +            saved_lines.append(line)
> +        if len(keeper) > 0:
> +            saved_lines += keeper
> +            keeper = []
> +    if len(saved_lines) > 0:
> +        yield saved_lines
> +
> +
> +def parse_patch(iter_lines, allow_dirty=False):
> +    '''
> +    :arg iter_lines: iterable of lines to parse
> +    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
> +        Default False
> +    '''
> +    iter_lines = iter_lines_handle_nl(iter_lines)
> +    try:
> +        (comments, orig_name, mod_name) = get_patch_names(iter_lines)
> +    except BinaryFiles, e:
> +        return BinaryPatch(e.comments, e.line)
> +    else:
> +        patch = Patch(comments, orig_name, mod_name)
> +        patch.comments = comments
> +        for hunk in iter_hunks(iter_lines, allow_dirty):
> +            patch.hunks.append(hunk)
> +        return patch
> +
> +
> +def parse_patches(iter_lines, allow_dirty=False):
> +    '''
> +    :arg iter_lines: iterable of lines to parse for patches
> +    :kwarg allow_dirty: If True, allow text that's not part of the patch at
> +        selected places.  This includes comments before and after a patch
> +        for instance.  Default False.
> +    '''
> +    patches = []
> +    for f in iter_file_patch(iter_lines, allow_dirty):
> +        patch = parse_patch(f.__iter__(), allow_dirty)
> +        patches.append(patch)
> +
> +    return patches
> 
> === added directory 'lib/lp/services/tests/data'
> === added file 'lib/lp/services/tests/data/diff-1'
> --- lib/lp/services/tests/data/diff-1	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/diff-1	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,30 @@
> +=== zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
> +--- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
> ++++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
> +@@ -70,7 +70,7 @@
> +                 "jf.bc=trgVayvarPbzzragf&cerivrjqvss_vq=1",
> +                 zbpxvb.erdhrfgf[0].pbasvt.qngn);
> +             zbpxvb.ynfg_erdhrfg = zbpxvb.erdhrfgf[0];
> +-            ine abj = (arj Qngr).inyhrBs()
> ++            ine abj = (arj Qngr).inyhrBs();
> +             choyvfurq_pbzzragf = [
> +                 {'yvar_ahzore': '2',
> +                  'crefba': crefba_bow,
> +@@ -80,7 +80,7 @@
> +                  'crefba': crefba_bow,
> +                  'grkg': 'Guvf vf terng.',
> +                  'qngr': (arj Qngr(abj - 12600000)),
> +-                },
> ++                }
> +             ];
> +             zbpxvb.fhpprff({
> +                 erfcbafrGrkg: L.WFBA.fgevatvsl(choyvfurq_pbzzragf),
> +@@ -88,7 +88,7 @@
> + 
> +             // Choyvfurq pbzzrag vf qvfcynlrq.
> +             ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
> +-            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
> ++            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
> +             L.Nffreg.nerRdhny(
> +                 'Sbb One (anzr16) jebgr ba 2012-08-12:',
> +                 svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
> 
> === added file 'lib/lp/services/tests/data/diff-2'
> --- lib/lp/services/tests/data/diff-2	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/diff-2	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,2 @@
> +=== nqqrq svyr 'jrohv/obql_ot.wct'
> +Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
> 
> === added file 'lib/lp/services/tests/data/diff-3'
> --- lib/lp/services/tests/data/diff-3	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/diff-3	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,12 @@
> +=== zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
> +--- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 12:32:44 +0000
> ++++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 23:47:16 +0000
> +@@ -3,7 +3,7 @@
> +     ine grfgf = L.anzrfcnpr('qngr.grfg');
> +     grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
> +
> +-    ine abj = (arj Qngr).inyhrBs()
> ++    ine abj = (arj Qngr).inyhrBs();
> +     grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
> +         anzr: 'grfg_nccebkvzngrqngr',
> +
> 
> === added file 'lib/lp/services/tests/data/expected-1'
> --- lib/lp/services/tests/data/expected-1	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-1	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,35 @@
> +
> +
> +Diff comments:
> +
> +> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
> +> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
> +> @@ -70,7 +70,7 @@
> +>                  "jf.bc=trgVayvarPbzzragf&cerivrjqvss_vq=1",
> +>                  zbpxvb.erdhrfgf[0].pbasvt.qngn);
> +>              zbpxvb.ynfg_erdhrfg = zbpxvb.erdhrfgf[0];
> +> -            ine abj = (arj Qngr).inyhrBs()
> +> +            ine abj = (arj Qngr).inyhrBs();
> +
> +Cool
> +
> +>              choyvfurq_pbzzragf = [
> +>                  {'yvar_ahzore': '2',
> +>                   'crefba': crefba_bow,
> +
> +> @@ -88,7 +88,7 @@
> +>  
> +>              // Choyvfurq pbzzrag vf qvfcynlrq.
> +>              ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
> +> -            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
> +> +            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
> +
> +Are you sure?
> +
> +>              L.Nffreg.nerRdhny(
> +>                  'Sbb One (anzr16) jebgr ba 2012-08-12:',
> +>                  svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/expected-2'
> --- lib/lp/services/tests/data/expected-2	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-2	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,12 @@
> +
> +
> +Diff comments:
> +
> +> === nqqrq svyr 'jrohv/obql_ot.wct'
> +> Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
> +
> +Why are you adding this?
> +
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/expected-3'
> --- lib/lp/services/tests/data/expected-3	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-3	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,22 @@
> +
> +
> +Diff comments:
> +
> +> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
> +> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 23:47:16 +0000
> +> @@ -3,7 +3,7 @@
> +>      ine grfgf = L.anzrfcnpr('qngr.grfg');
> +>      grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
> +>  
> +> -    ine abj = (arj Qngr).inyhrBs()
> +> +    ine abj = (arj Qngr).inyhrBs();
> +
> +What is this?
> +
> +>      grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
> +>          anzr: 'grfg_nccebkvzngrqngr',
> +>  
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/expected-4'
> --- lib/lp/services/tests/data/expected-4	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-4	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,14 @@
> +
> +
> +Diff comments:
> +
> +> === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'
> +> --- yvo/yc/pbqr/vagresnprf/qvss.cl      2009-10-01 13:25:12 +0000
> +
> +Is this from Planet Earth© ?
> +
> +> +++ yvo/yc/pbqr/vagresnprf/qvss.cl      2010-02-02 15:48:56 +0000
> +
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/expected-5'
> --- lib/lp/services/tests/data/expected-5	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-5	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,39 @@
> +
> +
> +Diff comments:
> +
> +> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
> +> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
> +
> +> @@ -80,7 +80,7 @@
> +>                   'crefba': crefba_bow,
> +>                   'grkg': 'Guvf vf terng.',
> +>                   'qngr': (arj Qngr(abj - 12600000)),
> +> -                },
> +> +                }
> +
> +Why?
> +
> +>              ];
> +>              zbpxvb.fhpprff({
> +>                  erfcbafrGrkg: L.WFBA.fgevatvsl(choyvfurq_pbzzragf),
> +
> +> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
> +> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 23:47:16 +0000
> +> @@ -3,7 +3,7 @@
> +>      ine grfgf = L.anzrfcnpr('qngr.grfg');
> +>      grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
> +>  
> +> -    ine abj = (arj Qngr).inyhrBs()
> +> +    ine abj = (arj Qngr).inyhrBs();
> +
> +Good catch!
> +
> +>      grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
> +>          anzr: 'grfg_nccebkvzngrqngr',
> +>  
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/expected-6'
> --- lib/lp/services/tests/data/expected-6	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/expected-6	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,45 @@
> +
> +
> +Diff comments:
> +
> +> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
> +> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
> +
> +> @@ -88,7 +88,7 @@
> +>  
> +>              // Choyvfurq pbzzrag vf qvfcynlrq.
> +>              ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
> +> -            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
> +> +            ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
> +
> ++1
> +
> +>              L.Nffreg.nerRdhny(
> +>                  'Sbb One (anzr16) jebgr ba 2012-08-12:',
> +>                  svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
> +> 
> +> === nqqrq svyr 'jrohv/obql_ot.wct'
> +> Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
> +
> +Do we need this?
> +
> +> 
> +> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
> +> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 12:32:44 +0000
> +> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf    2014-07-08 23:47:16 +0000
> +> @@ -3,7 +3,7 @@
> +>      ine grfgf = L.anzrfcnpr('qngr.grfg');
> +>      grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
> +>  
> +> -    ine abj = (arj Qngr).inyhrBs()
> +> +    ine abj = (arj Qngr).inyhrBs();
> +
> +Can we change this?
> +
> +>      grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
> +>          anzr: 'grfg_nccebkvzngrqngr',
> +>  
> +
> +
> +-- 
> 
> === added file 'lib/lp/services/tests/data/insert_top.patch'
> --- lib/lp/services/tests/data/insert_top.patch	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/insert_top.patch	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,7 @@
> +--- orig/pylon/patches.py
> ++++ mod/pylon/patches.py
> +@@ -1,3 +1,4 @@
> ++#test
> + import util
> + import sys
> + class PatchSyntax(Exception):
> 
> === added file 'lib/lp/services/tests/data/tricky-diff.patch'
> --- lib/lp/services/tests/data/tricky-diff.patch	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/data/tricky-diff.patch	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,26 @@
> +=== orig-7
> +--- orig-7
> ++++ mod-7
> +@@ -1,10 +1,10 @@
> + -- a
> +--- b
> ++++ c
> + xx d
> + xx e
> + ++ f
> +-++ g
> ++-- h
> + xx i
> + xx j
> + -- k
> +--- l
> ++++ m
> +=== orig-8
> +--- orig-8
> ++++ mod-8
> +@@ -1 +1 @@
> +--- A
> ++++ B
> +@@ -1 +1 @@
> +--- C
> ++++ D
> 
> === added file 'lib/lp/services/tests/test_patches.py'
> --- lib/lp/services/tests/test_patches.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/services/tests/test_patches.py	2014-07-26 02:00:47 +0000
> @@ -0,0 +1,129 @@
> +# Copyright 2014 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +import os
> +import re
> +
> +from bzrlib.errors import MalformedPatchHeader
> +from bzrlib.patches import difference_index
> +
> +from lp.services.patches import (
> +    BinaryPatch,
> +    get_patch_names,
> +    parse_patch,
> +    parse_patches,
> +    Patch,
> +    )
> +from lp.testing import TestCase
> +
> +
> +def datafile(filename):
> +    data_path = os.path.join(os.path.dirname(__file__),
> +                             "data", filename)
> +    return file(data_path, "rb")
> +
> +
> +class TestBzrLibCode(TestCase):
> +    """Test code rewritten from bzrlib.patches for diff comment emails"""
> +
> +    def assertContainsRe(self, haystack, needle_re, flags=0):
> +        """Assert that a contains something matching a regular expression."""
> +        if not re.search(needle_re, haystack, flags):
> +            if '\n' in haystack or len(haystack) > 60:
> +                # a long string, format it in a more readable way
> +                raise AssertionError(
> +                    'pattern "%s" not found in\n"""\\\n%s"""\n'
> +                    % (needle_re, haystack))
> +            else:
> +                raise AssertionError('pattern "%s" not found in "%s"'
> +                                     % (needle_re, haystack))
> +
> +    def data_lines(self, filename):
> +        data = datafile(filename)
> +        try:
> +            return data.readlines()
> +        finally:
> +            data.close()
> +
> +    def testValidPatchHeaderWithComments(self):
> +        """Parse a valid patch header"""
> +        expected_comments = ["=== modified file 'commands.py'"]
> +        lines = ("=== modified file 'commands.py'\n--- orig/commands.py\n"
> +                 "+++ mod/dommands.py\n").split('\n')
> +        (comments, orig, mod) = get_patch_names(lines.__iter__())
> +        self.assertEqual(comments, expected_comments)
> +        self.assertEqual(orig, "orig/commands.py")
> +        self.assertEqual(mod, "mod/dommands.py")
> +
> +    def testValidPatchHeader(self):
> +        """Parse a valid patch header"""
> +        expected_comments = []
> +        lines = ("--- orig/commands.py\n+++ mod/dommands.py\n").split('\n')
> +        (comments, orig, mod) = get_patch_names(lines.__iter__())
> +        self.assertEqual(comments, expected_comments)
> +        self.assertEqual(orig, "orig/commands.py")
> +        self.assertEqual(mod, "mod/dommands.py")
> +
> +    def testInvalidPatchHeader(self):
> +        """Parse an invalid patch header"""
> +        lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
> +        self.assertRaises(MalformedPatchHeader, get_patch_names,
> +                          lines.__iter__())
> +
> +    def compare_parsed(self, patchtext):
> +        lines = patchtext.splitlines(True)
> +        patch = parse_patch(lines.__iter__())
> +        pstr = str(patch)
> +        i = difference_index(patchtext, pstr)
> +        if i is not None:
> +            print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
> +        self.assertEqual(patchtext, str(patch))
> +
> +    def testAll(self):
> +        """Test parsing a whole patch"""
> +        patchtext = datafile("diff-1").read()
> +        self.compare_parsed(patchtext)
> +
> +    def test_parse_binary(self):
> +        """Test parsing a whole patch"""
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff = '\n'.join([diff_2, diff_1]).splitlines(True)
> +        patches = parse_patches(diff)
> +        self.assertIs(BinaryPatch, patches[0].__class__)
> +        self.assertIs(Patch, patches[1].__class__)
> +        self.assertContainsRe(
> +            str(patches[0]),
> +            'Binary files jrohv/obql_ot.wct1970-01-01.* and '
> +            'jrohv/obql_ot.wct19702014-01-31.* differ\n')
> +
> +    def test_parse_binary_after_normal(self):
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff = '\n'.join([diff_1, diff_2]).splitlines(True)
> +        patches = parse_patches(diff)
> +        self.assertIs(BinaryPatch, patches[1].__class__)
> +        self.assertIs(Patch, patches[0].__class__)
> +        self.assertContainsRe(
> +            str(patches[1]),
> +            'Binary files jrohv/obql_ot.wct1970-01-01.* and '
> +            'jrohv/obql_ot.wct19702014-01-31.* differ\n')
> +
> +    def test_roundtrip_binary(self):
> +        diff_1 = datafile("diff-1").read()
> +        diff_2 = datafile("diff-2").read()
> +        diff = '\n'.join([diff_2, diff_1])
> +        patches = parse_patches(diff.splitlines(True))
> +        self.assertEqual(diff, ''.join(str(p) for p in patches))
> +
> +    def testParsePatchesWithComments(self):
> +        """Make sure file names can be extracted from tricky unified diffs
> +        with comment lines"""
> +        patchtext = ''.join(self.data_lines("tricky-diff.patch"))
> +        filenames = [('orig-7', 'mod-7'),
> +                     ('orig-8', 'mod-8')]
> +        patches = parse_patches(patchtext.splitlines(True))
> +        patch_files = []
> +        for patch in patches:
> +            patch_files.append((patch.oldname, patch.newname))
> +        self.assertEqual(patch_files, filenames)
> 
> === modified file 'lib/lp/testing/factory.py'
> --- lib/lp/testing/factory.py	2014-07-18 07:43:06 +0000
> +++ lib/lp/testing/factory.py	2014-07-26 02:00:47 +0000
> @@ -1540,8 +1540,11 @@
>              Diff.fromFile(StringIO(diff_text), len(diff_text)))
>  
>      def makePreviewDiff(self, conflicts=u'', merge_proposal=None,
> -                        date_created=None):
> -        diff = self.makeDiff()
> +                        date_created=None, diff_text=None):
> +        if diff_text is None:
> +            diff = self.makeDiff()
> +        else:
> +            diff = self.makeDiff(diff_text)
>          if merge_proposal is None:
>              merge_proposal = self.makeBranchMergeProposal()
>          preview_diff = PreviewDiff()
> 


-- 
https://code.launchpad.net/~cjohnston/launchpad/short-ic-emails/+merge/225095
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References