Merge lp:~blr/bzr/bug-1400567 into lp:bzr

Proposed by Kit Randel
Status: Merged
Approved by: Richard Wilbur
Approved revision: no longer in the source branch.
Merged at revision: 6602
Proposed branch: lp:~blr/bzr/bug-1400567
Merge into: lp:bzr
Diff against target: 226 lines (+58/-20)
3 files modified
bzrlib/patches.py (+32/-9)
bzrlib/tests/test_patches.py (+22/-11)
doc/en/release-notes/bzr-2.7.txt (+4/-0)
To merge this branch: bzr merge lp:~blr/bzr/bug-1400567
Reviewer Review Type Date Requested Status
Richard Wilbur Approve
Vincent Ladeuil Approve
Review via email: mp+244531@code.launchpad.net

Commit message

Added keep_dirty kwarg to bzrlib.patches.parse_patches() allowing preservation of dirty patch headers.

Description of the change

Summary

Launchpad currently provides emails to users when inline diff comments are made in the context of a branch merge proposal. These emails tend to be very verbose (https://bugs.launchpad.net/launchpad/+bug/1334577) and a branch has been provided to try to address this (https://code.launchpad.net/~blr/launchpad/bug-1334577-verbose-diff/+merge/243751), by discarding patches and hunks without comments. The implementation parses launchpad diffs with bzrlib.patches, however in order for comment line numbers and diff line numbers to correspond correctly, bzrlib.patches need to be aware of dirty headers such as file 'state' headers that may precede a patch e.g.

    "=== added directory 'bish/bosh'"
    "=== modified file 'foo/bar.py'"

Proposed fix
The solution proposed in this branch is to add a keep_dirty kwarg to parse_patches(). When this is set to True, parse_patches will return a dict containing both the patch and its associated dirty_headers. This will allow us to reconstruct the original diff correctly in launchpad.

Tests
A relevant test has been added in test_patches.PatchesTester.test_preserve_dirty_head and can be run with:

./bzr selftest -v patches.PatchesTester

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

> ./bzr selftest -v patches.PatchesTester

Thanks so much for telling how to test the MP !

As a thank you hint: try './bzr selftest -v -s bt.test_patches' next time ;) The difference is that your command is less precise as it can match more tests, -s (--start-with) also avoid loading tests that won't be run

Overall, your approach is very clean and well tested.

I don't really like changing the return signature depending on an input parameter (especially for a generator) but I agree with your simpler approach, we can do refactoring later if needed (which is unclear) so that all call sites use your better returned values.

Thanks for the pep8 cleanups too which account for half of this proposal. Very much appreciated.

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :

Oh, forgot one bit: you need to add an news entry for that bugfix.

Revision history for this message
Kit Randel (blr) wrote :

Thanks Vincent!

Regarding the return signature, I felt similarly, but was concerned about
breaking the API.

In terms of adding a news entry, presumably I
ammend doc/en/release-notes/bzr-2.7.txt?

On Tue, Dec 16, 2014 at 12:55 AM, Vincent Ladeuil <email address hidden>
wrote:
>
> Oh, forgot one bit: you need to add an news entry for that bugfix.
> --
> https://code.launchpad.net/~blr/bzr/bug-1400567/+merge/244531
> You are the owner of lp:~blr/bzr/bug-1400567.
>

--
Kit Randel
Canonical - Ubuntu Engineering - Continuous Integration Team

Revision history for this message
Kit Randel (blr) wrote :

Added a release note under 'improvements' for 2.7b.

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Thanks for the news entry, the code cleanup (pep8), and the fix!
I approve.
+1

review: Approve
Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/patches.py'
--- bzrlib/patches.py 2011-12-18 15:28:38 +0000
+++ bzrlib/patches.py 2014-12-15 20:26:37 +0000
@@ -31,9 +31,10 @@
3131
32binary_files_re = 'Binary files (.*) and (.*) differ\n'32binary_files_re = 'Binary files (.*) and (.*) differ\n'
3333
34
34def get_patch_names(iter_lines):35def get_patch_names(iter_lines):
36 line = iter_lines.next()
35 try:37 try:
36 line = iter_lines.next()
37 match = re.match(binary_files_re, line)38 match = re.match(binary_files_re, line)
38 if match is not None:39 if match is not None:
39 raise BinaryFiles(match.group(1), match.group(2))40 raise BinaryFiles(match.group(1), match.group(2))
@@ -317,7 +318,6 @@
317 if isinstance(line, ContextLine):318 if isinstance(line, ContextLine):
318 pos += 1319 pos += 1
319320
320
321def parse_patch(iter_lines, allow_dirty=False):321def parse_patch(iter_lines, allow_dirty=False):
322 '''322 '''
323 :arg iter_lines: iterable of lines to parse323 :arg iter_lines: iterable of lines to parse
@@ -336,7 +336,7 @@
336 return patch336 return patch
337337
338338
339def iter_file_patch(iter_lines, allow_dirty=False):339def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
340 '''340 '''
341 :arg iter_lines: iterable of lines to parse for patches341 :arg iter_lines: iterable of lines to parse for patches
342 :kwarg allow_dirty: If True, allow comments and other non-patch text342 :kwarg allow_dirty: If True, allow comments and other non-patch text
@@ -352,10 +352,15 @@
352 # (as allow_dirty does).352 # (as allow_dirty does).
353 regex = re.compile(binary_files_re)353 regex = re.compile(binary_files_re)
354 saved_lines = []354 saved_lines = []
355 dirty_head = []
355 orig_range = 0356 orig_range = 0
356 beginning = True357 beginning = True
358
357 for line in iter_lines:359 for line in iter_lines:
358 if line.startswith('=== ') or line.startswith('*** '):360 if line.startswith('=== '):
361 dirty_head.append(line)
362 continue
363 if line.startswith('*** '):
359 continue364 continue
360 if line.startswith('#'):365 if line.startswith('#'):
361 continue366 continue
@@ -369,14 +374,23 @@
369 # parse the patch374 # parse the patch
370 beginning = False375 beginning = False
371 elif len(saved_lines) > 0:376 elif len(saved_lines) > 0:
372 yield saved_lines377 if keep_dirty and len(dirty_head) > 0:
378 yield {'saved_lines': saved_lines,
379 'dirty_head': dirty_head}
380 dirty_head = []
381 else:
382 yield saved_lines
373 saved_lines = []383 saved_lines = []
374 elif line.startswith('@@'):384 elif line.startswith('@@'):
375 hunk = hunk_from_header(line)385 hunk = hunk_from_header(line)
376 orig_range = hunk.orig_range386 orig_range = hunk.orig_range
377 saved_lines.append(line)387 saved_lines.append(line)
378 if len(saved_lines) > 0:388 if len(saved_lines) > 0:
379 yield saved_lines389 if keep_dirty and len(dirty_head) > 0:
390 yield {'saved_lines': saved_lines,
391 'dirty_head': dirty_head}
392 else:
393 yield saved_lines
380394
381395
382def iter_lines_handle_nl(iter_lines):396def iter_lines_handle_nl(iter_lines):
@@ -400,15 +414,24 @@
400 yield last_line414 yield last_line
401415
402416
403def parse_patches(iter_lines, allow_dirty=False):417def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
404 '''418 '''
405 :arg iter_lines: iterable of lines to parse for patches419 :arg iter_lines: iterable of lines to parse for patches
406 :kwarg allow_dirty: If True, allow text that's not part of the patch at420 :kwarg allow_dirty: If True, allow text that's not part of the patch at
407 selected places. This includes comments before and after a patch421 selected places. This includes comments before and after a patch
408 for instance. Default False.422 for instance. Default False.
423 :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
424 Default False.
409 '''425 '''
410 return [parse_patch(f.__iter__(), allow_dirty) for f in426 patches = []
411 iter_file_patch(iter_lines, allow_dirty)]427 for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
428 if 'dirty_head' in patch_lines:
429 patches.append({'patch': parse_patch(
430 patch_lines['saved_lines'], allow_dirty),
431 'dirty_head': patch_lines['dirty_head']})
432 else:
433 patches.append(parse_patch(patch_lines, allow_dirty))
434 return patches
412435
413436
414def difference_index(atext, btext):437def difference_index(atext, btext):
415438
=== modified file 'bzrlib/tests/test_patches.py'
--- bzrlib/tests/test_patches.py 2010-05-20 18:23:17 +0000
+++ bzrlib/tests/test_patches.py 2014-12-15 20:26:37 +0000
@@ -58,10 +58,24 @@
58 # https://bugs.launchpad.net/bzr/+bug/50207658 # https://bugs.launchpad.net/bzr/+bug/502076
59 # https://code.launchpad.net/~toshio/bzr/allow-dirty-patches/+merge/1885459 # https://code.launchpad.net/~toshio/bzr/allow-dirty-patches/+merge/18854
60 lines = ["diff -pruN commands.py",60 lines = ["diff -pruN commands.py",
61 "--- orig/commands.py",61 "--- orig/commands.py",
62 "+++ mod/dommands.py"]62 "+++ mod/dommands.py"]
63 bits = parse_patches(iter(lines), allow_dirty=True)63 bits = parse_patches(iter(lines), allow_dirty=True)
6464
65 def test_preserve_dirty_head(self):
66 """Parse a patch containing a dirty header, and preserve lines"""
67 lines = ["=== added directory 'foo/bar'\n",
68 "=== modified file 'orig/commands.py'\n",
69 "--- orig/commands.py\n",
70 "+++ mod/dommands.py\n"]
71 patches = parse_patches(
72 lines.__iter__(), allow_dirty=True, keep_dirty=True)
73 self.assertEqual(patches[0]['dirty_head'],
74 ["=== added directory 'foo/bar'\n",
75 "=== modified file 'orig/commands.py'\n"])
76 self.assertEqual(patches[0]['patch'].get_header().splitlines(True),
77 ["--- orig/commands.py\n", "+++ mod/dommands.py\n"])
78
65 def testValidPatchHeader(self):79 def testValidPatchHeader(self):
66 """Parse a valid patch header"""80 """Parse a valid patch header"""
67 lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')81 lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
@@ -78,7 +92,7 @@
78 def testValidHunkHeader(self):92 def testValidHunkHeader(self):
79 """Parse a valid hunk header"""93 """Parse a valid hunk header"""
80 header = "@@ -34,11 +50,6 @@\n"94 header = "@@ -34,11 +50,6 @@\n"
81 hunk = hunk_from_header(header);95 hunk = hunk_from_header(header)
82 self.assertEqual(hunk.orig_pos, 34)96 self.assertEqual(hunk.orig_pos, 34)
83 self.assertEqual(hunk.orig_range, 11)97 self.assertEqual(hunk.orig_range, 11)
84 self.assertEqual(hunk.mod_pos, 50)98 self.assertEqual(hunk.mod_pos, 50)
@@ -88,7 +102,7 @@
88 def testValidHunkHeader2(self):102 def testValidHunkHeader2(self):
89 """Parse a tricky, valid hunk header"""103 """Parse a tricky, valid hunk header"""
90 header = "@@ -1 +0,0 @@\n"104 header = "@@ -1 +0,0 @@\n"
91 hunk = hunk_from_header(header);105 hunk = hunk_from_header(header)
92 self.assertEqual(hunk.orig_pos, 1)106 self.assertEqual(hunk.orig_pos, 1)
93 self.assertEqual(hunk.orig_range, 1)107 self.assertEqual(hunk.orig_range, 1)
94 self.assertEqual(hunk.mod_pos, 0)108 self.assertEqual(hunk.mod_pos, 0)
@@ -117,7 +131,7 @@
117 self.makeMalformed("@@ -34,11 +50,6.5 @@\n")131 self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
118 self.makeMalformed("@@ -34,11 +50,-6 @@\n")132 self.makeMalformed("@@ -34,11 +50,-6 @@\n")
119133
120 def lineThing(self,text, type):134 def lineThing(self, text, type):
121 line = parse_line(text)135 line = parse_line(text)
122 self.assertIsInstance(line, type)136 self.assertIsInstance(line, type)
123 self.assertEqual(str(line), text)137 self.assertEqual(str(line), text)
@@ -146,7 +160,7 @@
146 i = difference_index(patchtext, pstr)160 i = difference_index(patchtext, pstr)
147 if i is not None:161 if i is not None:
148 print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])162 print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
149 self.assertEqual (patchtext, str(patch))163 self.assertEqual(patchtext, str(patch))
150164
151 def testAll(self):165 def testAll(self):
152 """Test parsing a whole patch"""166 """Test parsing a whole patch"""
@@ -161,7 +175,7 @@
161 self.assertContainsRe(patches[0].oldname, '^bar\t')175 self.assertContainsRe(patches[0].oldname, '^bar\t')
162 self.assertContainsRe(patches[0].newname, '^qux\t')176 self.assertContainsRe(patches[0].newname, '^qux\t')
163 self.assertContainsRe(str(patches[0]),177 self.assertContainsRe(str(patches[0]),
164 'Binary files bar\t.* and qux\t.* differ\n')178 'Binary files bar\t.* and qux\t.* differ\n')
165179
166 def test_parse_binary_after_normal(self):180 def test_parse_binary_after_normal(self):
167 patches = parse_patches(self.data_lines("binary-after-normal.patch"))181 patches = parse_patches(self.data_lines("binary-after-normal.patch"))
@@ -170,7 +184,7 @@
170 self.assertContainsRe(patches[1].oldname, '^bar\t')184 self.assertContainsRe(patches[1].oldname, '^bar\t')
171 self.assertContainsRe(patches[1].newname, '^qux\t')185 self.assertContainsRe(patches[1].newname, '^qux\t')
172 self.assertContainsRe(str(patches[1]),186 self.assertContainsRe(str(patches[1]),
173 'Binary files bar\t.* and qux\t.* differ\n')187 'Binary files bar\t.* and qux\t.* differ\n')
174188
175 def test_roundtrip_binary(self):189 def test_roundtrip_binary(self):
176 patchtext = ''.join(self.data_lines("binary.patch"))190 patchtext = ''.join(self.data_lines("binary.patch"))
@@ -228,7 +242,6 @@
228 mod_lines = list(self.datafile(mod))242 mod_lines = list(self.datafile(mod))
229243
230 patched_file = IterableFile(iter_patched(orig_lines, patch))244 patched_file = IterableFile(iter_patched(orig_lines, patch))
231 lines = []
232 count = 0245 count = 0
233 for patch_line in patched_file:246 for patch_line in patched_file:
234 self.assertEqual(patch_line, mod_lines[count])247 self.assertEqual(patch_line, mod_lines[count])
@@ -239,7 +252,6 @@
239 binary_lines = self.data_lines('binary.patch')252 binary_lines = self.data_lines('binary.patch')
240 e = self.assertRaises(BinaryFiles, iter_patched, [], binary_lines)253 e = self.assertRaises(BinaryFiles, iter_patched, [], binary_lines)
241254
242
243 def test_iter_patched_from_hunks(self):255 def test_iter_patched_from_hunks(self):
244 """Test a few patch files, and make sure they work."""256 """Test a few patch files, and make sure they work."""
245 files = [257 files = [
@@ -256,7 +268,6 @@
256 mod_lines = list(self.datafile(mod))268 mod_lines = list(self.datafile(mod))
257 iter_patched = iter_patched_from_hunks(orig_lines, parsed.hunks)269 iter_patched = iter_patched_from_hunks(orig_lines, parsed.hunks)
258 patched_file = IterableFile(iter_patched)270 patched_file = IterableFile(iter_patched)
259 lines = []
260 count = 0271 count = 0
261 for patch_line in patched_file:272 for patch_line in patched_file:
262 self.assertEqual(patch_line, mod_lines[count])273 self.assertEqual(patch_line, mod_lines[count])
263274
=== modified file 'doc/en/release-notes/bzr-2.7.txt'
--- doc/en/release-notes/bzr-2.7.txt 2014-06-19 09:42:08 +0000
+++ doc/en/release-notes/bzr-2.7.txt 2014-12-15 20:26:37 +0000
@@ -26,6 +26,10 @@
26.. Improvements to existing commands, especially improved performance 26.. Improvements to existing commands, especially improved performance
27 or memory usage, or better results.27 or memory usage, or better results.
2828
29* bzrlib.patches.parse_patches can optionally return a list of 'dirty'
30 patch headers (prefixed with '===').
31 (Kit Randel, #1400567)
32
29Bug Fixes33Bug Fixes
30*********34*********
3135