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

Proposed by Chris Johnston
Status: Rejected
Rejected by: William Grant
Proposed branch: lp:~cjohnston/launchpad/short-ic-emails
Merge into: lp:launchpad
Diff against target: 1111 lines (+883/-60)
17 files modified
lib/lp/code/mail/codereviewcomment.py (+47/-12)
lib/lp/code/mail/tests/test_codereviewcomment.py (+189/-46)
lib/lp/services/patches.py (+215/-0)
lib/lp/services/tests/data/diff-1 (+30/-0)
lib/lp/services/tests/data/diff-2 (+2/-0)
lib/lp/services/tests/data/diff-3 (+14/-0)
lib/lp/services/tests/data/expected-1 (+35/-0)
lib/lp/services/tests/data/expected-2 (+12/-0)
lib/lp/services/tests/data/expected-3 (+22/-0)
lib/lp/services/tests/data/expected-4 (+14/-0)
lib/lp/services/tests/data/expected-5 (+39/-0)
lib/lp/services/tests/data/expected-6 (+44/-0)
lib/lp/services/tests/data/expected-7 (+55/-0)
lib/lp/services/tests/data/insert_top.patch (+7/-0)
lib/lp/services/tests/data/tricky-diff.patch (+26/-0)
lib/lp/services/tests/test_patches.py (+127/-0)
lib/lp/testing/factory.py (+5/-2)
To merge this branch: bzr merge lp:~cjohnston/launchpad/short-ic-emails
Reviewer Review Type Date Requested Status
William Grant code Needs Fixing
Celso Providelo (community) Approve
Review via email: mp+225095@code.launchpad.net

Commit message

First pass at reducing the amount of code that is emailed out when someone saves diff comments

Description of the change

This branch makes a first pass at reducing the amount of code that is emailed out when someone saves diff comments. The way this is done is by discarding hunks that do not have diff comments.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Needs Fixing (code)
Revision history for this message
Celso Providelo (cprov) wrote :

Chris,

The code seems to be working as expected, code hunks without comments are suppressed in the email.

However, there are few points that needs consideration IMO:

* Code copied from bzrlib needs to be removed (reused from bzrlib, maybe with accessors) or, in last instance, copied to a different and isolated place.

* Test coverage seems to be enough but the testing infrastructure (bzrlib TestCase should not be needed) and test names and descriptions needs update.

* The way we load test inputs and outputs from files is nice to make the tests less noisy, but at the same time makes it very hard to understand. I don't think there is an easy solution here, but surely the problem can be alleviated with better test description(i.e. 'binary file mentions in diff are properly presented.')

* Finally, the core code changes (build_inline_comments_section) can be simplified by replacing the boolean flags with continue and some use some local functions. That would make the code clearer.

review: Needs Information
Revision history for this message
Celso Providelo (cprov) wrote :

lib/lp/services/patches.py is a much better place, the code style there still alien to LP, it reads very differently. But I am not opposed to landing it this way if you think it will be closer to the original bzrlib, thus easier to maintain up to date.

Looks good for landing and get some feedback from users, possibly experimenting with including previous comments too.

Good work!

review: Approve
Revision history for this message
William Grant (wgrant) wrote :

lp.services.patches should say somewhere that it's derived from bzrlib.patches, what the changes are, and why. It might also be possible to push these improvements back to bzrlib.patches in a backward-compatible way later.

The new set of test files doesn't give any tests for inline comments across multiple files, just multiple hunks. That should probably be fixed.

Revision history for this message
William Grant (wgrant) wrote :

Consider '\n'.join(whatever) instead of '{}{}{}'.format(whatever) to get a more realistic diff with blank lines between files.

Revision history for this message
William Grant (wgrant) wrote :

Line numbers jump ahead by one at the boundary between file, as visible in expected-6: the comment in the second file is rendered a line too late, and the comment in the third is two lines late. I assume the trailing newline isn't being counted.

Also, it probably makes sense to have a gap between hunks, at least when one is skipped. In expected-6, the hunk at 70 seems to lead directly into 88, but in the original there's one at 80.

Additionally, does expected-1 give any value over expected-6?

review: Needs Fixing (code)
Revision history for this message
Chris Johnston (cjohnston) wrote :

> Also, it probably makes sense to have a gap between hunks, at least when one
> is skipped. In expected-6, the hunk at 70 seems to lead directly into 88, but
> in the original there's one at 80.

How do you suggest doing the gap? A blank line or a line that starts with '>', or maybe even some sort of text to indicate that a hunk is skipped? Also, how do you want to handle if multiple hunks in a row (or even a whole file) are mising? One line for each hunk missing, one line total, if a file is missing and it's the first file should we start out with a blank line? I almost think that we should see what type of feedback we get the way things are and then iterate again.

> Additionally, does expected-1 give any value over expected-6?

Just the fact that it's a very simple diff with only one file, so just trying to cover one more case where things are different.

Revision history for this message
William Grant (wgrant) wrote :

This is now broken in the opposite direction. If there is no gap between
two files, the comments end up a line too early.

On 16/07/14 03:35, Chris Johnston wrote:
>> Also, it probably makes sense to have a gap between hunks, at least when one
>> is skipped. In expected-6, the hunk at 70 seems to lead directly into 88, but
>> in the original there's one at 80.
>
> How do you suggest doing the gap? A blank line or a line that starts with '>', or maybe even some sort of text to indicate that a hunk is skipped? Also, how do you want to handle if multiple hunks in a row (or even a whole file) are mising? One line for each hunk missing, one line total, if a file is missing and it's the first file should we start out with a blank line? I almost think that we should see what type of feedback we get the way things are and then iterate again.

I think just a blank line makes sense. A line starting with ">" would
indicate that it was quoting the diff, which is false.

Revision history for this message
Chris Johnston (cjohnston) wrote :

I have updated the MP to handle a nl between patches or no nl between patches. I also added a blank line when there is a skipped hunk.

Revision history for this message
William Grant (wgrant) :
Revision history for this message
William Grant (wgrant) wrote :

This breaks three tests in TestInlineCommentsSection, and I have a few other comments.

review: Needs Fixing (code)
Revision history for this message
Chris Johnston (cjohnston) :
Revision history for this message
William Grant (wgrant) :
Revision history for this message
Chris Johnston (cjohnston) :
Revision history for this message
Chris Johnston (cjohnston) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-29 15:41:46 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Email notifications for code review comments."""4"""Email notifications for code review comments."""
@@ -26,6 +26,7 @@
26 append_footer,26 append_footer,
27 format_address,27 format_address,
28 )28 )
29from lp.services.patches import parse_patches
29from lp.services.webapp import canonical_url30from lp.services.webapp import canonical_url
3031
3132
@@ -171,16 +172,50 @@
171172
172def build_inline_comments_section(comments, diff_text):173def build_inline_comments_section(comments, diff_text):
173 """Return a formatted text section with contextualized comments."""174 """Return a formatted text section with contextualized comments."""
174 result_lines = []175 result_diff = []
175 diff_lines = diff_text.splitlines()176 lines = diff_text.splitlines(True)
176 for num, line in enumerate(diff_lines, 1):177 patches = parse_patches(lines, allow_dirty=True)
177 result_lines.append(u'> {0}'.format(line.decode('utf-8', 'replace')))178 line_count = 0
178 comment = comments.get(str(num))179 for patch in patches:
179 if comment is not None:180 patch_lines = []
180 result_lines.append('')181 patch_has_ic = False
181 result_lines.extend(comment.splitlines())182 headers = patch.get_header()
182 result_lines.append('')183 has_header_ic = False
183184 header_lines = []
184 result_text = u'\n'.join(result_lines)185 for line in headers.splitlines():
186 line_count += 1
187 comment = comments.get(str(line_count))
188 header_lines.append(u'> {0}'.format(
189 line.decode('utf-8', 'replace')))
190 if comment is not None:
191 header_lines.append('')
192 header_lines.extend(comment.splitlines())
193 header_lines.append('')
194 has_header_ic = True
195
196 for p in patch.hunks:
197 hunk_lines = []
198 has_ic = False
199 raw_diff = str(p).splitlines()
200 for num, line in enumerate(raw_diff, 1):
201 line_count += 1
202 hunk_lines.append(u'> {0}'.format(
203 line.decode('utf-8', 'replace')))
204 comment = comments.get(str(line_count))
205 if comment is not None:
206 hunk_lines.append('')
207 hunk_lines.extend(comment.splitlines())
208 hunk_lines.append('')
209 has_ic = True
210 if has_ic:
211 patch_lines += hunk_lines
212 patch_has_ic = True
213 else:
214 if not patch_lines or patch_lines[-1] != u'':
215 patch_lines.append('')
216 if has_header_ic or patch_has_ic:
217 result_diff += header_lines + patch_lines
218
219 result_text = u'\n'.join(result_diff)
185220
186 return '\n\nDiff comments:\n\n%s\n\n' % result_text221 return '\n\nDiff comments:\n\n%s\n\n' % result_text
187222
=== 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-29 15:41:46 +0000
@@ -1,8 +1,9 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test CodeReviewComment emailing functionality."""4"""Test CodeReviewComment emailing functionality."""
55
6import os
67
7import testtools8import testtools
8import transaction9import transaction
@@ -31,6 +32,12 @@
31from lp.testing.layers import LaunchpadFunctionalLayer32from lp.testing.layers import LaunchpadFunctionalLayer
3233
3334
35def datafile(filename):
36 data_path = os.path.join(os.path.dirname(__file__),
37 "../../../services/tests/data", filename)
38 return file(data_path, "rb")
39
40
34class TestCodeReviewComment(TestCaseWithFactory):41class TestCodeReviewComment(TestCaseWithFactory):
35 """Test that comments are generated as expected."""42 """Test that comments are generated as expected."""
3643
@@ -215,13 +222,14 @@
215 mailer.message.text_contents.splitlines())222 mailer.message.text_contents.splitlines())
216223
217 def makeCommentWithInlineComments(self, subject=None, content=None,224 def makeCommentWithInlineComments(self, subject=None, content=None,
218 inline_comments=None):225 inline_comments=None, diff_text=None):
219 """Create a `CodeReviewComment` with inline (diff) comments."""226 """Create a `CodeReviewComment` with inline (diff) comments."""
220 bmp = self.factory.makeBranchMergeProposal()227 bmp = self.factory.makeBranchMergeProposal()
221 bmp.source_branch.subscribe(bmp.registrant,228 bmp.source_branch.subscribe(bmp.registrant,
222 BranchSubscriptionNotificationLevel.NOEMAIL, None,229 BranchSubscriptionNotificationLevel.NOEMAIL, None,
223 CodeReviewNotificationLevel.FULL, bmp.registrant)230 CodeReviewNotificationLevel.FULL, bmp.registrant)
224 previewdiff = self.factory.makePreviewDiff(merge_proposal=bmp)231 previewdiff = self.factory.makePreviewDiff(merge_proposal=bmp,
232 diff_text=diff_text)
225 transaction.commit()233 transaction.commit()
226 if subject is None:234 if subject is None:
227 subject = 'A comment'235 subject = 'A comment'
@@ -235,14 +243,180 @@
235 inline_comments=inline_comments)243 inline_comments=inline_comments)
236 return comment244 return comment
237245
238 def test_generateEmailWithInlineComments(self):246 # For the test_inline_comments_email_* tests, please see
239 """Review comments emails consider the inline comments.247 # `build_inline_comments_section` tests for formatting details.
240248
241 See `build_inline_comments_section` tests for formatting details.249 def test_inline_comments_email_text(self):
242 """250 # Testing that a single file with three hunks is parsed properly and
243 # Enabled corresponding feature flag.251 # only the header/hunk combinations with comments are provided.
244 self.useContext(feature_flags())252
245 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')253 # Enabled corresponding feature flag.
254 self.useContext(feature_flags())
255 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
256 diff = datafile("diff-1").read()
257 expected = datafile("expected-1").read().decode().splitlines()
258
259 comment = self.makeCommentWithInlineComments(
260 inline_comments={'9': u'Cool',
261 '27': u'Are you sure?'},
262 diff_text=diff)
263 mailer = CodeReviewCommentMailer.forCreation(comment)
264 commenter = comment.branch_merge_proposal.registrant
265 ctrl = mailer.generateEmail(
266 commenter.preferredemail.email, commenter)
267
268 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
269
270 def test_inline_comments_email_text_binary(self):
271 # Testing that a single file with three hunks and a binary file is
272 # parsed properly and only the binary header with comments are provided
273
274 # Enabled corresponding feature flag.
275 self.useContext(feature_flags())
276 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
277 diff_1 = datafile("diff-1").read()
278 diff_2 = datafile("diff-2").read()
279 diff = u'\n'.join([diff_1, diff_2])
280 expected = datafile("expected-2").read().decode().splitlines()
281
282 comment = self.makeCommentWithInlineComments(
283 inline_comments={'33': u'Why are you adding this?'},
284 diff_text=diff)
285 mailer = CodeReviewCommentMailer.forCreation(comment)
286 commenter = comment.branch_merge_proposal.registrant
287 ctrl = mailer.generateEmail(
288 commenter.preferredemail.email, commenter)
289
290 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
291
292 def test_inline_comments_email_binary(self):
293 # Testing that a binary file is parsed properly and only the binary
294 # header with comments are provided
295
296 # Enabled corresponding feature flag.
297 self.useContext(feature_flags())
298 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
299 diff = datafile("diff-2").read()
300 expected = datafile("expected-2").read().decode().splitlines()
301
302 comment = self.makeCommentWithInlineComments(
303 inline_comments={'2': u'Why are you adding this?'},
304 diff_text=diff)
305 mailer = CodeReviewCommentMailer.forCreation(comment)
306 commenter = comment.branch_merge_proposal.registrant
307 ctrl = mailer.generateEmail(
308 commenter.preferredemail.email, commenter)
309
310 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
311
312 def test_inline_comments_email_text_binary_text(self):
313 # Testing that a single file with three hunks, a binary file and a
314 # text file is parsed properly and only the binary header with comments
315 # are provided
316
317 # Enabled corresponding feature flag.
318 self.useContext(feature_flags())
319 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
320 diff_1 = datafile("diff-1").read()
321 diff_3 = datafile("diff-3").read()
322 diff = u'\n'.join([diff_1, diff_3])
323 expected = datafile("expected-3").read().decode().splitlines()
324
325 comment = self.makeCommentWithInlineComments(
326 inline_comments={'42': u'What is this?'},
327 diff_text=diff)
328 mailer = CodeReviewCommentMailer.forCreation(comment)
329 commenter = comment.branch_merge_proposal.registrant
330 ctrl = mailer.generateEmail(
331 commenter.preferredemail.email, commenter)
332
333 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
334
335 def test_inline_comments_email_text_binary_text_includes_two_files(self):
336 # Testing that a single file with three hunks, a binary file and a
337 # text file is parsed properly and a single hunk from the first file
338 # plus a single hunk from the second file with comments are provided
339
340 # Enabled corresponding feature flag.
341 self.useContext(feature_flags())
342 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
343 diff_1 = datafile("diff-1").read()
344 diff_3 = datafile("diff-3").read()
345 diff = u'\n'.join([diff_1, diff_3])
346 expected = datafile("expected-5").read().decode().splitlines()
347
348 comment = self.makeCommentWithInlineComments(
349 inline_comments={'18': u'Why?',
350 '42': u'Good catch!'},
351 diff_text=diff)
352 mailer = CodeReviewCommentMailer.forCreation(comment)
353 commenter = comment.branch_merge_proposal.registrant
354 ctrl = mailer.generateEmail(
355 commenter.preferredemail.email, commenter)
356
357 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
358
359 def test_inline_comments_email_text_binary_text_includes_three_files(self):
360 # Testing that a single file with three hunks, a binary file and a
361 # text file is parsed properly and two hunks from the first file,
362 # the second file and a single hunk from the second file with comments
363 # are provided
364
365 # Enabled corresponding feature flag.
366 self.useContext(feature_flags())
367 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
368 diff_1 = datafile("diff-1").read()
369 diff_3 = datafile("diff-3").read()
370 diff = u'\n'.join([diff_1, diff_3])
371 expected = datafile("expected-6").read().decode().splitlines()
372
373 comment = self.makeCommentWithInlineComments(
374 inline_comments={'27': u'+1',
375 '33': u'Do we need this?',
376 '42': u'Can we change this?'},
377 diff_text=diff)
378 mailer = CodeReviewCommentMailer.forCreation(comment)
379 commenter = comment.branch_merge_proposal.registrant
380 ctrl = mailer.generateEmail(
381 commenter.preferredemail.email, commenter)
382
383 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
384
385 def test_inline_comments_email_text_binary_text_no_nl_files(self):
386 # Testing that a single file with three hunks, a binary file and a
387 # text file which do not have a new line between each other is parsed
388 # properly and two hunks from the first file, the second file and a
389 # single hunk from the second file with comments are provided
390
391 # Enabled corresponding feature flag.
392 self.useContext(feature_flags())
393 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
394 diff_1 = datafile("diff-1").read()
395 diff_3 = datafile("diff-3").read()
396 diff = u''.join([diff_1, diff_3])
397 expected = datafile("expected-7").read().decode().splitlines()
398
399 comment = self.makeCommentWithInlineComments(
400 inline_comments={'9': u'Good stuff',
401 '27': u'+1',
402 '32': u'Do we need this?',
403 '41': u'Can we change this?'},
404 diff_text=diff)
405 mailer = CodeReviewCommentMailer.forCreation(comment)
406 commenter = comment.branch_merge_proposal.registrant
407 ctrl = mailer.generateEmail(
408 commenter.preferredemail.email, commenter)
409
410 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
411
412 def test_inline_comments_email_text_comment_in_header(self):
413 # Testing that a single file with comments in the header is parsed
414 # properly and only the headers is provided
415
416 # Enabled corresponding feature flag.
417 self.useContext(feature_flags())
418 set_feature_flag(u'code.inline_diff_comments.enabled', u'enabled')
419 expected = datafile("expected-4").read().decode('utf-8').splitlines()
246420
247 comment = self.makeCommentWithInlineComments(421 comment = self.makeCommentWithInlineComments(
248 inline_comments={'2': u'Is this from Planet Earth\xa9 ?'})422 inline_comments={'2': u'Is this from Planet Earth\xa9 ?'})
@@ -251,20 +425,7 @@
251 ctrl = mailer.generateEmail(425 ctrl = mailer.generateEmail(
252 commenter.preferredemail.email, commenter)426 commenter.preferredemail.email, commenter)
253427
254 expected_lines = [428 self.assertEqual(expected, ctrl.body.splitlines()[:-2])
255 '',
256 'Diff comments:',
257 '',
258 "> === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'",
259 '> --- yvo/yc/pbqr/vagresnprf/qvss.cl '
260 '2009-10-01 13:25:12 +0000',
261 '',
262 u'Is this from Planet Earth\xa9 ?',
263 '',
264 '> +++ yvo/yc/pbqr/vagresnprf/qvss.cl '
265 '2010-02-02 15:48:56 +0000'
266 ]
267 self.assertEqual(expected_lines, ctrl.body.splitlines()[1:10])
268429
269 def test_generateEmailWithInlineComments_feature_disabled(self):430 def test_generateEmailWithInlineComments_feature_disabled(self):
270 """Inline comments are not considered if the flag is not enabled."""431 """Inline comments are not considered if the flag is not enabled."""
@@ -425,24 +586,6 @@
425 diff_text = self.diff_text586 diff_text = self.diff_text
426 return build_inline_comments_section(comments, diff_text)587 return build_inline_comments_section(comments, diff_text)
427588
428 def test_section_header_and_footer(self):
429 # The inline comments section starts with a 4-lines header
430 # (empty lines and title) and ends with an empty line.
431 section = self.getSection({}).splitlines()
432 header = section[:5]
433 self.assertEqual(
434 ['',
435 '',
436 'Diff comments:',
437 '',
438 '> --- bar\t2009-08-26 15:53:34.000000000 -0400'],
439 header)
440 footer = section[-2:]
441 self.assertEqual(
442 ['> +e',
443 ''],
444 footer)
445
446 def test_single_line_comment(self):589 def test_single_line_comment(self):
447 # The inline comments are correctly contextualized in the diff.590 # The inline comments are correctly contextualized in the diff.
448 # and prefixed with '>>> '591 # and prefixed with '>>> '
@@ -452,8 +595,8 @@
452 '',595 '',
453 u'\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae',596 u'\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae',
454 '',597 '',
455 '> @@ -1,3 +0,0 @@',598 '',
456 u'> -\xe5'],599 u''],
457 self.getSection(comments).splitlines()[5:11])600 self.getSection(comments).splitlines()[5:11])
458601
459 def test_multi_line_comment(self):602 def test_multi_line_comment(self):
@@ -465,7 +608,7 @@
465 'Foo',608 'Foo',
466 'Bar',609 'Bar',
467 '',610 '',
468 '> @@ -1,3 +0,0 @@'],611 ''],
469 self.getSection(comments).splitlines()[5:11])612 self.getSection(comments).splitlines()[5:11])
470613
471 def test_multiple_comments(self):614 def test_multiple_comments(self):
472615
=== 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-29 15:41:46 +0000
@@ -0,0 +1,215 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# This file is derived from bzrlib.patches in order to extend some
5# functionality for use by the Launchpad inline comment feature.
6
7# In order to reduce the size of a diff being emailed with inline comments
8# to just the relevant hunks of code, it was needed to break the diff down
9# and then put the required pieces back together. To do this, the notion of
10# 'comments' was added to Patch and BinaryPatch in order to return any lines
11# beginning with '===' which is later added to the file headers and the line
12# counted.
13
14import re
15
16from bzrlib.errors import (
17 BzrError,
18 MalformedHunkHeader,
19 MalformedPatchHeader,
20 PatchSyntax,
21 )
22from bzrlib.patches import (
23 binary_files_re,
24 ContextLine,
25 hunk_from_header,
26 InsertLine,
27 iter_lines_handle_nl,
28 parse_line,
29 RemoveLine,
30 )
31
32
33binary_regex = re.compile(binary_files_re)
34
35
36class BinaryFiles(BzrError):
37
38 _fmt = 'Binary files section encountered.'
39
40 def __init__(self, comments, orig_name, mod_name):
41 self.comments = comments
42 self.orig_name = orig_name
43 self.mod_name = mod_name
44
45
46class BinaryPatch(object):
47 def __init__(self, comments, oldname, newname):
48 self.oldname = oldname
49 self.newname = newname
50 self.comments = comments
51 self.hunks = []
52
53 def __str__(self):
54 return self.get_header()
55
56 def get_header(self):
57 header = 'Binary files %s and %s differ\n' % (
58 self.oldname, self.newname)
59 return ''.join(self.comments + [header])
60
61
62class Patch(BinaryPatch):
63 def __init__(self, comments, oldname, newname):
64 BinaryPatch.__init__(self, comments, oldname, newname)
65 self.hunks = []
66 self.comments = comments
67
68 def __str__(self):
69 ret = self.get_header()
70 ret += "".join([str(h) for h in self.hunks])
71 return ret
72
73 def get_header(self):
74 comments = ''.join(self.comments)
75 return "%s--- %s\n+++ %s\n" % (comments, self.oldname, self.newname)
76
77
78def get_patch_names(iter_lines):
79 comments = []
80 for line in iter_lines:
81 if (line == "\n" or line.startswith('=== ') or line.startswith('*** ')
82 or line.startswith('#')):
83 comments.append(line)
84 else:
85 break
86 try:
87 match = binary_regex.match(line)
88 if match is not None:
89 # For binary packages we need to check and see if there is a \n
90 # that follows the header info. If so, we need to carry it.
91 raise BinaryFiles(comments, match.group(1), match.group(2))
92 if not line.startswith("--- "):
93 raise MalformedPatchHeader("No orig name", line)
94 else:
95 orig_name = line[4:].rstrip("\n")
96 except StopIteration:
97 raise MalformedPatchHeader("No orig line", "")
98 try:
99 line = iter_lines.next()
100 if not line.startswith("+++ "):
101 raise PatchSyntax("No mod name")
102 else:
103 mod_name = line[4:].rstrip("\n")
104 except StopIteration:
105 raise MalformedPatchHeader("No mod line", "")
106 return (comments, orig_name, mod_name)
107
108
109def iter_hunks(iter_lines, allow_dirty=False):
110 '''
111 :arg iter_lines: iterable of lines to parse for hunks
112 :kwarg allow_dirty: If True, when we encounter something that is not
113 a hunk header when we're looking for one, assume the rest of the lines
114 are not part of the patch (comments or other junk). Default False
115 '''
116 hunk = None
117 for line in iter_lines:
118 if line == "\n":
119 if hunk is not None:
120 hunk.lines.append(line)
121 yield hunk
122 hunk = None
123 continue
124 if hunk is not None:
125 yield hunk
126 try:
127 hunk = hunk_from_header(line)
128 except MalformedHunkHeader:
129 if allow_dirty:
130 # If the line isn't a hunk header, then we've reached the end
131 # of this patch and there's "junk" at the end. Ignore the
132 # rest of this patch.
133 return
134 raise
135 orig_size = 0
136 mod_size = 0
137 while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
138 hunk_line = parse_line(iter_lines.next())
139 hunk.lines.append(hunk_line)
140 if isinstance(hunk_line, (RemoveLine, ContextLine)):
141 orig_size += 1
142 if isinstance(hunk_line, (InsertLine, ContextLine)):
143 mod_size += 1
144 if hunk is not None:
145 yield hunk
146
147
148def iter_file_patch(iter_lines, allow_dirty=False):
149 '''
150 :arg iter_lines: iterable of lines to parse for patches
151 :kwarg allow_dirty: If True, allow comments and other non-patch text
152 before the first patch. Note that the algorithm here can only find
153 such text before any patches have been found. Comments after the
154 first patch are stripped away in iter_hunks() if it is also passed
155 allow_dirty=True. Default False.
156 '''
157 ### FIXME: Docstring is not quite true. We allow certain comments no
158 # matter what, If they startwith '===', '***', or '#' Someone should
159 # reexamine this logic and decide if we should include those in
160 # allow_dirty or restrict those to only being before the patch is found
161 # (as allow_dirty does).
162 saved_lines = []
163 for line in iter_lines:
164 # if it's a === or binary it's a new patch, but we might have
165 # both together.
166 if line.startswith('=== '):
167 # assume a new patch until we know differently
168 if len(saved_lines) > 0:
169 yield saved_lines
170 saved_lines = []
171 saved_lines.append(line)
172 elif binary_regex.match(line):
173 # check for preceeding '=== ' line
174 # a binary patch always yields the previous patch
175 if len(saved_lines) > 0:
176 saved_lines.append(line)
177 else:
178 yield [line]
179 else:
180 saved_lines.append(line)
181 if len(saved_lines) > 0:
182 yield saved_lines
183
184
185def parse_patch(iter_lines, allow_dirty=False):
186 '''
187 :arg iter_lines: iterable of lines to parse
188 :kwarg allow_dirty: If True, allow the patch to have trailing junk.
189 Default False
190 '''
191 iter_lines = iter_lines_handle_nl(iter_lines)
192 try:
193 (comments, orig_name, mod_name) = get_patch_names(iter_lines)
194 except BinaryFiles, e:
195 return BinaryPatch(e.comments, e.orig_name, e.mod_name)
196 else:
197 patch = Patch(comments, orig_name, mod_name)
198 for hunk in iter_hunks(iter_lines, allow_dirty):
199 patch.hunks.append(hunk)
200 return patch
201
202
203def parse_patches(iter_lines, allow_dirty=False):
204 '''
205 :arg iter_lines: iterable of lines to parse for patches
206 :kwarg allow_dirty: If True, allow text that's not part of the patch at
207 selected places. This includes comments before and after a patch
208 for instance. Default False.
209 '''
210 patches = []
211 for f in iter_file_patch(iter_lines, allow_dirty):
212 patch = parse_patch(f.__iter__(), allow_dirty)
213 patches.append(patch)
214
215 return patches
0216
=== 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-29 15:41:46 +0000
@@ -0,0 +1,30 @@
1=== zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
2--- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
3+++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
4@@ -70,7 +70,7 @@
5 "jf.bc=trgVayvarPbzzragf&cerivrjqvss_vq=1",
6 zbpxvb.erdhrfgf[0].pbasvt.qngn);
7 zbpxvb.ynfg_erdhrfg = zbpxvb.erdhrfgf[0];
8- ine abj = (arj Qngr).inyhrBs()
9+ ine abj = (arj Qngr).inyhrBs();
10 choyvfurq_pbzzragf = [
11 {'yvar_ahzore': '2',
12 'crefba': crefba_bow,
13@@ -80,7 +80,7 @@
14 'crefba': crefba_bow,
15 'grkg': 'Guvf vf terng.',
16 'qngr': (arj Qngr(abj - 12600000)),
17- },
18+ }
19 ];
20 zbpxvb.fhpprff({
21 erfcbafrGrkg: L.WFBA.fgevatvsl(choyvfurq_pbzzragf),
22@@ -88,7 +88,7 @@
23
24 // Choyvfurq pbzzrag vf qvfcynlrq.
25 ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
26- ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
27+ ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
28 L.Nffreg.nerRdhny(
29 'Sbb One (anzr16) jebgr ba 2012-08-12:',
30 svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
031
=== 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-29 15:41:46 +0000
@@ -0,0 +1,2 @@
1=== nqqrq svyr 'jrohv/obql_ot.wct'
2Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
03
=== 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-29 15:41:46 +0000
@@ -0,0 +1,14 @@
1=== nqqrq svyr 'jrohv/obql_ot.wct'
2Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
3=== zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
4--- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 12:32:44 +0000
5+++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 23:47:16 +0000
6@@ -3,7 +3,7 @@
7 ine grfgf = L.anzrfcnpr('qngr.grfg');
8 grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
9
10- ine abj = (arj Qngr).inyhrBs()
11+ ine abj = (arj Qngr).inyhrBs();
12 grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
13 anzr: 'grfg_nccebkvzngrqngr',
14
015
=== 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-29 15:41:46 +0000
@@ -0,0 +1,35 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
6> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
7> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
8> @@ -70,7 +70,7 @@
9> "jf.bc=trgVayvarPbzzragf&cerivrjqvss_vq=1",
10> zbpxvb.erdhrfgf[0].pbasvt.qngn);
11> zbpxvb.ynfg_erdhrfg = zbpxvb.erdhrfgf[0];
12> - ine abj = (arj Qngr).inyhrBs()
13> + ine abj = (arj Qngr).inyhrBs();
14
15Cool
16
17> choyvfurq_pbzzragf = [
18> {'yvar_ahzore': '2',
19> 'crefba': crefba_bow,
20
21> @@ -88,7 +88,7 @@
22>
23> // Choyvfurq pbzzrag vf qvfcynlrq.
24> ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
25> - ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
26> + ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
27
28Are you sure?
29
30> L.Nffreg.nerRdhny(
31> 'Sbb One (anzr16) jebgr ba 2012-08-12:',
32> svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
33
34
35--
036
=== 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-29 15:41:46 +0000
@@ -0,0 +1,12 @@
1
2
3Diff comments:
4
5> === nqqrq svyr 'jrohv/obql_ot.wct'
6> Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
7
8Why are you adding this?
9
10
11
12--
013
=== 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-29 15:41:46 +0000
@@ -0,0 +1,22 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
6> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 12:32:44 +0000
7> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 23:47:16 +0000
8> @@ -3,7 +3,7 @@
9> ine grfgf = L.anzrfcnpr('qngr.grfg');
10> grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
11>
12> - ine abj = (arj Qngr).inyhrBs()
13> + ine abj = (arj Qngr).inyhrBs();
14
15What is this?
16
17> grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
18> anzr: 'grfg_nccebkvzngrqngr',
19>
20
21
22--
023
=== 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-29 15:41:46 +0000
@@ -0,0 +1,14 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'
6> --- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000
7
8Is this from Planet Earth© ?
9
10> +++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000
11
12
13
14--
015
=== 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-29 15:41:46 +0000
@@ -0,0 +1,39 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
6> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
7> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
8
9> @@ -80,7 +80,7 @@
10> 'crefba': crefba_bow,
11> 'grkg': 'Guvf vf terng.',
12> 'qngr': (arj Qngr(abj - 12600000)),
13> - },
14> + }
15
16Why?
17
18> ];
19> zbpxvb.fhpprff({
20> erfcbafrGrkg: L.WFBA.fgevatvsl(choyvfurq_pbzzragf),
21
22> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
23> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 12:32:44 +0000
24> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 23:47:16 +0000
25> @@ -3,7 +3,7 @@
26> ine grfgf = L.anzrfcnpr('qngr.grfg');
27> grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
28>
29> - ine abj = (arj Qngr).inyhrBs()
30> + ine abj = (arj Qngr).inyhrBs();
31
32Good catch!
33
34> grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
35> anzr: 'grfg_nccebkvzngrqngr',
36>
37
38
39--
040
=== 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-29 15:41:46 +0000
@@ -0,0 +1,44 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
6> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
7> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
8
9> @@ -88,7 +88,7 @@
10>
11> // Choyvfurq pbzzrag vf qvfcynlrq.
12> ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
13> - ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
14> + ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
15
16+1
17
18> L.Nffreg.nerRdhny(
19> 'Sbb One (anzr16) jebgr ba 2012-08-12:',
20> svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
21>
22> === nqqrq svyr 'jrohv/obql_ot.wct'
23> Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
24
25Do we need this?
26
27> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
28> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 12:32:44 +0000
29> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 23:47:16 +0000
30> @@ -3,7 +3,7 @@
31> ine grfgf = L.anzrfcnpr('qngr.grfg');
32> grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
33>
34> - ine abj = (arj Qngr).inyhrBs()
35> + ine abj = (arj Qngr).inyhrBs();
36
37Can we change this?
38
39> grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
40> anzr: 'grfg_nccebkvzngrqngr',
41>
42
43
44--
045
=== added file 'lib/lp/services/tests/data/expected-7'
--- lib/lp/services/tests/data/expected-7 1970-01-01 00:00:00 +0000
+++ lib/lp/services/tests/data/expected-7 2014-07-29 15:41:46 +0000
@@ -0,0 +1,55 @@
1
2
3Diff comments:
4
5> === zbqvsvrq svyr 'yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf'
6> --- yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 12:32:44 +0000
7> +++ yvo/yc/pbqr/wninfpevcg/grfgf/grfg_oenapuzretrcebcbfny.vayvarpbzzragf.wf 2014-07-08 23:47:16 +0000
8> @@ -70,7 +70,7 @@
9> "jf.bc=trgVayvarPbzzragf&cerivrjqvss_vq=1",
10> zbpxvb.erdhrfgf[0].pbasvt.qngn);
11> zbpxvb.ynfg_erdhrfg = zbpxvb.erdhrfgf[0];
12> - ine abj = (arj Qngr).inyhrBs()
13> + ine abj = (arj Qngr).inyhrBs();
14
15Good stuff
16
17> choyvfurq_pbzzragf = [
18> {'yvar_ahzore': '2',
19> 'crefba': crefba_bow,
20
21> @@ -88,7 +88,7 @@
22>
23> // Choyvfurq pbzzrag vf qvfcynlrq.
24> ine svefg_pbzzragf = L.bar('#qvss-yvar-2').arkg().bar('qvi');
25> - ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq')
26> + ine svefg = svefg_pbzzragf.bar('qvi:svefg-puvyq');
27
28+1
29
30> L.Nffreg.nerRdhny(
31> 'Sbb One (anzr16) jebgr ba 2012-08-12:',
32> svefg.bar('.obneqPbzzragQrgnvyf').trg('grkg'));
33> === nqqrq svyr 'jrohv/obql_ot.wct'
34> Binary files jrohv/obql_ot.wct1970-01-01 00:00:00 +0000 and jrohv/obql_ot.wct19702014-01-31 16:57:19 +0000 differ
35
36Do we need this?
37
38> === zbqvsvrq svyr 'yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf'
39> --- yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 12:32:44 +0000
40> +++ yvo/yc/ncc/wninfpevcg/grfgf/grfg_qngr.wf 2014-07-08 23:47:16 +0000
41> @@ -3,7 +3,7 @@
42> ine grfgf = L.anzrfcnpr('qngr.grfg');
43> grfgf.fhvgr = arj L.Grfg.Fhvgr("qngr grfgf");
44>
45> - ine abj = (arj Qngr).inyhrBs()
46> + ine abj = (arj Qngr).inyhrBs();
47
48Can we change this?
49
50> grfgf.fhvgr.nqq(arj L.Grfg.Pnfr({
51> anzr: 'grfg_nccebkvzngrqngr',
52>
53
54
55--
056
=== 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-29 15:41:46 +0000
@@ -0,0 +1,7 @@
1--- orig/pylon/patches.py
2+++ mod/pylon/patches.py
3@@ -1,3 +1,4 @@
4+#test
5 import util
6 import sys
7 class PatchSyntax(Exception):
08
=== 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-29 15:41:46 +0000
@@ -0,0 +1,26 @@
1=== orig-7
2--- orig-7
3+++ mod-7
4@@ -1,10 +1,10 @@
5 -- a
6--- b
7+++ c
8 xx d
9 xx e
10 ++ f
11-++ g
12+-- h
13 xx i
14 xx j
15 -- k
16--- l
17+++ m
18=== orig-8
19--- orig-8
20+++ mod-8
21@@ -1 +1 @@
22--- A
23+++ B
24@@ -1 +1 @@
25--- C
26+++ D
027
=== 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-29 15:41:46 +0000
@@ -0,0 +1,127 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4import os
5import re
6
7from bzrlib.errors import MalformedPatchHeader
8from bzrlib.patches import difference_index
9
10from lp.services.patches import (
11 BinaryPatch,
12 get_patch_names,
13 parse_patch,
14 parse_patches,
15 Patch,
16 )
17from lp.testing import TestCase
18
19
20def datafile(filename):
21 data_path = os.path.join(os.path.dirname(__file__),
22 "data", filename)
23 return file(data_path, "rb")
24
25
26class TestBzrLibCode(TestCase):
27 """Test code rewritten from bzrlib.patches for diff comment emails"""
28
29 def assertContainsRe(self, haystack, needle_re, flags=0):
30 """Assert that a contains something matching a regular expression."""
31 if not re.search(needle_re, haystack, flags):
32 if '\n' in haystack or len(haystack) > 60:
33 # a long string, format it in a more readable way
34 raise AssertionError(
35 'pattern "%s" not found in\n"""\\\n%s"""\n'
36 % (needle_re, haystack))
37 else:
38 raise AssertionError('pattern "%s" not found in "%s"'
39 % (needle_re, haystack))
40
41 def data_lines(self, filename):
42 data = datafile(filename)
43 try:
44 return data.readlines()
45 finally:
46 data.close()
47
48 def testValidPatchHeaderWithComments(self):
49 """Parse a valid patch header"""
50 expected_comments = ["=== modified file 'commands.py'"]
51 lines = ("=== modified file 'commands.py'\n--- orig/commands.py\n"
52 "+++ mod/dommands.py\n").split('\n')
53 (comments, orig, mod) = get_patch_names(lines.__iter__())
54 self.assertEqual(comments, expected_comments)
55 self.assertEqual(orig, "orig/commands.py")
56 self.assertEqual(mod, "mod/dommands.py")
57
58 def testValidPatchHeader(self):
59 """Parse a valid patch header"""
60 expected_comments = []
61 lines = ("--- orig/commands.py\n+++ mod/dommands.py\n").split('\n')
62 (comments, orig, mod) = get_patch_names(lines.__iter__())
63 self.assertEqual(comments, expected_comments)
64 self.assertEqual(orig, "orig/commands.py")
65 self.assertEqual(mod, "mod/dommands.py")
66
67 def testInvalidPatchHeader(self):
68 """Parse an invalid patch header"""
69 lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
70 self.assertRaises(MalformedPatchHeader, get_patch_names,
71 lines.__iter__())
72
73 def compare_parsed(self, patchtext):
74 lines = patchtext.splitlines(True)
75 patch = parse_patch(lines.__iter__())
76 pstr = str(patch)
77 i = difference_index(patchtext, pstr)
78 if i is not None:
79 print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
80 self.assertEqual(patchtext, str(patch))
81
82 def testAll(self):
83 """Test parsing a whole patch"""
84 patchtext = datafile("diff-1").read()
85 self.compare_parsed(patchtext)
86
87 def test_parse_binary(self):
88 """Test parsing a whole patch"""
89 diff_1 = datafile("diff-1").read()
90 diff_2 = datafile("diff-2").read()
91 diff = '\n'.join([diff_2, diff_1]).splitlines(True)
92 patches = parse_patches(diff)
93 self.assertIs(BinaryPatch, patches[0].__class__)
94 self.assertIs(Patch, patches[1].__class__)
95 self.assertContainsRe(
96 str(patches[0]),
97 'Binary files jrohv/obql_ot.wct1970-01-01.* and '
98 'jrohv/obql_ot.wct19702014-01-31.* differ\n')
99
100 def test_parse_binary_after_normal(self):
101 diff_1 = datafile("diff-1").read()
102 diff_2 = datafile("diff-2").read()
103 diff = '\n'.join([diff_1, diff_2]).splitlines(True)
104 patches = parse_patches(diff)
105 self.assertIs(BinaryPatch, patches[1].__class__)
106 self.assertIs(Patch, patches[0].__class__)
107 self.assertContainsRe(
108 str(patches[1]),
109 'Binary files jrohv/obql_ot.wct1970-01-01.* and '
110 'jrohv/obql_ot.wct19702014-01-31.* differ\n')
111
112 def test_roundtrip_binary(self):
113 diff = datafile("diff-3").read()
114 patches = parse_patches(diff.splitlines(True))
115 self.assertEqual(diff, ''.join(str(p) for p in patches))
116
117 def testParsePatchesWithComments(self):
118 """Make sure file names can be extracted from tricky unified diffs
119 with comment lines"""
120 patchtext = ''.join(self.data_lines("tricky-diff.patch"))
121 filenames = [('orig-7', 'mod-7'),
122 ('orig-8', 'mod-8')]
123 patches = parse_patches(patchtext.splitlines(True))
124 patch_files = []
125 for patch in patches:
126 patch_files.append((patch.oldname, patch.newname))
127 self.assertEqual(patch_files, filenames)
0128
=== 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-29 15:41:46 +0000
@@ -1540,8 +1540,11 @@
1540 Diff.fromFile(StringIO(diff_text), len(diff_text)))1540 Diff.fromFile(StringIO(diff_text), len(diff_text)))
15411541
1542 def makePreviewDiff(self, conflicts=u'', merge_proposal=None,1542 def makePreviewDiff(self, conflicts=u'', merge_proposal=None,
1543 date_created=None):1543 date_created=None, diff_text=None):
1544 diff = self.makeDiff()1544 if diff_text is None:
1545 diff = self.makeDiff()
1546 else:
1547 diff = self.makeDiff(diff_text)
1545 if merge_proposal is None:1548 if merge_proposal is None:
1546 merge_proposal = self.makeBranchMergeProposal()1549 merge_proposal = self.makeBranchMergeProposal()
1547 preview_diff = PreviewDiff()1550 preview_diff = PreviewDiff()