Merge lp:~cjwatson/launchpad/simplify-code-mail-patches into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 19051
Proposed branch: lp:~cjwatson/launchpad/simplify-code-mail-patches
Merge into: lp:launchpad
Diff against target: 478 lines (+9/-406)
2 files modified
lib/lp/code/mail/codereviewcomment.py (+4/-4)
lib/lp/code/mail/patches.py (+5/-402)
To merge this branch: bzr merge lp:~cjwatson/launchpad/simplify-code-mail-patches
Reviewer Review Type Date Requested Status
Simon Davy (community) Approve
Launchpad code reviewers Pending
Review via email: mp+372793@code.launchpad.net

Commit message

Remove parts of lp.code.mail.patches that are unmodified from bzrlib.

Description of the change

lp.code.mail.patches was cloned-and-hacked from bzrlib.patches in order to support git diffs, but at the time we were in a rush and ended up copying the whole module rather than just the parts we needed to change. This removes all the parts that were identical except for trivial details like whitespace.

This will simplify porting to breezy.

To post a comment you must log in.
Revision history for this message
Simon Davy (bloodearnest) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/mail/codereviewcomment.py'
2--- lib/lp/code/mail/codereviewcomment.py 2018-08-17 12:54:24 +0000
3+++ lib/lp/code/mail/codereviewcomment.py 2019-09-14 00:01:49 +0000
4@@ -10,6 +10,7 @@
5 'CodeReviewCommentMailer',
6 ]
7
8+from bzrlib.patches import BinaryPatch
9 from zope.component import getUtility
10 from zope.security.proxy import removeSecurityProxy
11
12@@ -20,8 +21,8 @@
13 from lp.code.interfaces.codereviewinlinecomment import (
14 ICodeReviewInlineCommentSet,
15 )
16-from lp.code.mail import patches
17 from lp.code.mail.branchmergeproposal import BMPMailer
18+from lp.code.mail.patches import parse_patches
19 from lp.services.mail.sendmail import (
20 append_footer,
21 format_address,
22@@ -188,8 +189,7 @@
23 """
24 diff_lines = diff_text.splitlines(True)
25
26- diff_patches = patches.parse_patches(
27- diff_lines, allow_dirty=True, keep_dirty=True)
28+ diff_patches = parse_patches(diff_lines, allow_dirty=True, keep_dirty=True)
29 result_lines = []
30 line_count = 0 # track lines in original diff
31
32@@ -210,7 +210,7 @@
33 patch = patch['patch']
34
35 # call type here as patch is an instance of both Patch and BinaryPatch
36- if type(patch) is patches.BinaryPatch:
37+ if type(patch) is BinaryPatch:
38 if dirty_comment:
39 result_lines.extend(dirty_head)
40 result_lines.append(u'> %s' % str(patch).rstrip('\n'))
41
42=== modified file 'lib/lp/code/mail/patches.py'
43--- lib/lp/code/mail/patches.py 2015-10-27 13:03:25 +0000
44+++ lib/lp/code/mail/patches.py 2019-09-14 00:01:49 +0000
45@@ -1,4 +1,4 @@
46-# This file was cloned from bzr-2.6.0-lp-3 (bzrlib.patches) and
47+# This file was partially cloned from bzr-2.6.0-lp-3 (bzrlib.patches) and
48 # customised for LP.
49 #
50 # Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
51@@ -20,327 +20,15 @@
52
53 from __future__ import absolute_import
54
55-from bzrlib.errors import (
56- BinaryFiles,
57- MalformedHunkHeader,
58- MalformedLine,
59- MalformedPatchHeader,
60- PatchConflict,
61- PatchSyntax,
62+from bzrlib.patches import (
63+ binary_files_re,
64+ hunk_from_header,
65+ parse_patch,
66 )
67
68 import re
69
70
71-binary_files_re = 'Binary files (.*) and (.*) differ\n'
72-
73-
74-def get_patch_names(iter_lines):
75- line = iter_lines.next()
76- try:
77- match = re.match(binary_files_re, line)
78- if match is not None:
79- raise BinaryFiles(match.group(1), match.group(2))
80- if not line.startswith("--- "):
81- raise MalformedPatchHeader("No orig name", line)
82- else:
83- orig_name = line[4:].rstrip("\n")
84- except StopIteration:
85- raise MalformedPatchHeader("No orig line", "")
86- try:
87- line = iter_lines.next()
88- if not line.startswith("+++ "):
89- raise PatchSyntax("No mod name")
90- else:
91- mod_name = line[4:].rstrip("\n")
92- except StopIteration:
93- raise MalformedPatchHeader("No mod line", "")
94- return (orig_name, mod_name)
95-
96-
97-def parse_range(textrange):
98- """Parse a patch range, handling the "1" special-case
99-
100- :param textrange: The text to parse
101- :type textrange: str
102- :return: the position and range, as a tuple
103- :rtype: (int, int)
104- """
105- tmp = textrange.split(',')
106- if len(tmp) == 1:
107- pos = tmp[0]
108- range = "1"
109- else:
110- (pos, range) = tmp
111- pos = int(pos)
112- range = int(range)
113- return (pos, range)
114-
115-
116-def hunk_from_header(line):
117- import re
118- matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
119- if matches is None:
120- raise MalformedHunkHeader("Does not match format.", line)
121- try:
122- (orig, mod) = matches.group(1).split(" ")
123- except (ValueError, IndexError), e:
124- raise MalformedHunkHeader(str(e), line)
125- if not orig.startswith('-') or not mod.startswith('+'):
126- raise MalformedHunkHeader("Positions don't start with + or -.", line)
127- try:
128- (orig_pos, orig_range) = parse_range(orig[1:])
129- (mod_pos, mod_range) = parse_range(mod[1:])
130- except (ValueError, IndexError), e:
131- raise MalformedHunkHeader(str(e), line)
132- if mod_range < 0 or orig_range < 0:
133- raise MalformedHunkHeader("Hunk range is negative", line)
134- tail = matches.group(3)
135- return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
136-
137-
138-class HunkLine:
139- def __init__(self, contents):
140- self.contents = contents
141-
142- def get_str(self, leadchar):
143- if self.contents == "\n" and leadchar == " " and False:
144- return "\n"
145- if not self.contents.endswith('\n'):
146- terminator = '\n' + NO_NL
147- else:
148- terminator = ''
149- return leadchar + self.contents + terminator
150-
151-
152-class ContextLine(HunkLine):
153- def __init__(self, contents):
154- HunkLine.__init__(self, contents)
155-
156- def __str__(self):
157- return self.get_str(" ")
158-
159-
160-class InsertLine(HunkLine):
161- def __init__(self, contents):
162- HunkLine.__init__(self, contents)
163-
164- def __str__(self):
165- return self.get_str("+")
166-
167-
168-class RemoveLine(HunkLine):
169- def __init__(self, contents):
170- HunkLine.__init__(self, contents)
171-
172- def __str__(self):
173- return self.get_str("-")
174-
175-NO_NL = '\\ No newline at end of file\n'
176-__pychecker__ = "no-returnvalues"
177-
178-
179-def parse_line(line):
180- if line.startswith("\n"):
181- return ContextLine(line)
182- elif line.startswith(" "):
183- return ContextLine(line[1:])
184- elif line.startswith("+"):
185- return InsertLine(line[1:])
186- elif line.startswith("-"):
187- return RemoveLine(line[1:])
188- else:
189- raise MalformedLine("Unknown line type", line)
190-__pychecker__ = ""
191-
192-
193-class Hunk:
194- def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
195- self.orig_pos = orig_pos
196- self.orig_range = orig_range
197- self.mod_pos = mod_pos
198- self.mod_range = mod_range
199- self.tail = tail
200- self.lines = []
201-
202- def get_header(self):
203- if self.tail is None:
204- tail_str = ''
205- else:
206- tail_str = ' ' + self.tail
207- return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
208- self.orig_range),
209- self.range_str(self.mod_pos,
210- self.mod_range),
211- tail_str)
212-
213- def range_str(self, pos, range):
214- """Return a file range, special-casing for 1-line files.
215-
216- :param pos: The position in the file
217- :type pos: int
218- :range: The range in the file
219- :type range: int
220- :return: a string in the format 1,4 except when range == pos == 1
221- """
222- if range == 1:
223- return "%i" % pos
224- else:
225- return "%i,%i" % (pos, range)
226-
227- def __str__(self):
228- lines = [self.get_header()]
229- for line in self.lines:
230- lines.append(str(line))
231- return "".join(lines)
232-
233- def shift_to_mod(self, pos):
234- if pos < self.orig_pos - 1:
235- return 0
236- elif pos > self.orig_pos + self.orig_range:
237- return self.mod_range - self.orig_range
238- else:
239- return self.shift_to_mod_lines(pos)
240-
241- def shift_to_mod_lines(self, pos):
242- position = self.orig_pos - 1
243- shift = 0
244- for line in self.lines:
245- if isinstance(line, InsertLine):
246- shift += 1
247- elif isinstance(line, RemoveLine):
248- if position == pos:
249- return None
250- shift -= 1
251- position += 1
252- elif isinstance(line, ContextLine):
253- position += 1
254- if position > pos:
255- break
256- return shift
257-
258-
259-def iter_hunks(iter_lines, allow_dirty=False):
260- '''
261- :arg iter_lines: iterable of lines to parse for hunks
262- :kwarg allow_dirty: If True, when we encounter something that is not
263- a hunk header when we're looking for one, assume the rest of the lines
264- are not part of the patch (comments or other junk). Default False
265- '''
266- hunk = None
267- for line in iter_lines:
268- if line == "\n":
269- if hunk is not None:
270- yield hunk
271- hunk = None
272- continue
273- if hunk is not None:
274- yield hunk
275- try:
276- hunk = hunk_from_header(line)
277- except MalformedHunkHeader:
278- if allow_dirty:
279- # If the line isn't a hunk header, then we've reached the end
280- # of this patch and there's "junk" at the end. Ignore the
281- # rest of this patch.
282- return
283- raise
284- orig_size = 0
285- mod_size = 0
286- while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
287- hunk_line = parse_line(iter_lines.next())
288- hunk.lines.append(hunk_line)
289- if isinstance(hunk_line, (RemoveLine, ContextLine)):
290- orig_size += 1
291- if isinstance(hunk_line, (InsertLine, ContextLine)):
292- mod_size += 1
293- if hunk is not None:
294- yield hunk
295-
296-
297-class BinaryPatch(object):
298- def __init__(self, oldname, newname):
299- self.oldname = oldname
300- self.newname = newname
301-
302- def __str__(self):
303- return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
304-
305-
306-class Patch(BinaryPatch):
307-
308- def __init__(self, oldname, newname):
309- BinaryPatch.__init__(self, oldname, newname)
310- self.hunks = []
311-
312- def __str__(self):
313- ret = self.get_header()
314- ret += "".join([str(h) for h in self.hunks])
315- return ret
316-
317- def get_header(self):
318- return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
319-
320- def stats_values(self):
321- """Calculate the number of inserts and removes."""
322- removes = 0
323- inserts = 0
324- for hunk in self.hunks:
325- for line in hunk.lines:
326- if isinstance(line, InsertLine):
327- inserts += 1
328- elif isinstance(line, RemoveLine):
329- removes += 1
330- return (inserts, removes, len(self.hunks))
331-
332- def stats_str(self):
333- """Return a string of patch statistics"""
334- return "%i inserts, %i removes in %i hunks" % \
335- self.stats_values()
336-
337- def pos_in_mod(self, position):
338- newpos = position
339- for hunk in self.hunks:
340- shift = hunk.shift_to_mod(position)
341- if shift is None:
342- return None
343- newpos += shift
344- return newpos
345-
346- def iter_inserted(self):
347- """Iteraties through inserted lines
348-
349- :return: Pair of line number, line
350- :rtype: iterator of (int, InsertLine)
351- """
352- for hunk in self.hunks:
353- pos = hunk.mod_pos - 1
354- for line in hunk.lines:
355- if isinstance(line, InsertLine):
356- yield (pos, line)
357- pos += 1
358- if isinstance(line, ContextLine):
359- pos += 1
360-
361-
362-def parse_patch(iter_lines, allow_dirty=False):
363- '''
364- :arg iter_lines: iterable of lines to parse
365- :kwarg allow_dirty: If True, allow the patch to have trailing junk.
366- Default False
367- '''
368- iter_lines = iter_lines_handle_nl(iter_lines)
369- try:
370- (orig_name, mod_name) = get_patch_names(iter_lines)
371- except BinaryFiles, e:
372- return BinaryPatch(e.orig_name, e.mod_name)
373- else:
374- patch = Patch(orig_name, mod_name)
375- for hunk in iter_hunks(iter_lines, allow_dirty):
376- patch.hunks.append(hunk)
377- return patch
378-
379-
380 def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
381 '''
382 :arg iter_lines: iterable of lines to parse for patches
383@@ -419,27 +107,6 @@
384 yield saved_lines
385
386
387-def iter_lines_handle_nl(iter_lines):
388- """
389- Iterates through lines, ensuring that lines that originally had no
390- terminating \n are produced without one. This transformation may be
391- applied at any point up until hunk line parsing, and is safe to apply
392- repeatedly.
393- """
394- last_line = None
395- for line in iter_lines:
396- if line == NO_NL:
397- if not last_line.endswith('\n'):
398- raise AssertionError()
399- last_line = last_line[:-1]
400- line = None
401- if last_line is not None:
402- yield last_line
403- last_line = line
404- if last_line is not None:
405- yield last_line
406-
407-
408 def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
409 '''
410 :arg iter_lines: iterable of lines to parse for patches
411@@ -458,67 +125,3 @@
412 else:
413 patches.append(parse_patch(patch_lines, allow_dirty))
414 return patches
415-
416-
417-def difference_index(atext, btext):
418- """Find the indext of the first character that differs between two texts
419-
420- :param atext: The first text
421- :type atext: str
422- :param btext: The second text
423- :type str: str
424- :return: The index, or None if there are no differences within the range
425- :rtype: int or NoneType
426- """
427- length = len(atext)
428- if len(btext) < length:
429- length = len(btext)
430- for i in range(length):
431- if atext[i] != btext[i]:
432- return i
433- return None
434-
435-
436-def iter_patched(orig_lines, patch_lines):
437- """Iterate through a series of lines with a patch applied.
438- This handles a single file, and does exact, not fuzzy patching.
439- """
440- patch_lines = iter_lines_handle_nl(iter(patch_lines))
441- get_patch_names(patch_lines)
442- return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
443-
444-
445-def iter_patched_from_hunks(orig_lines, hunks):
446- """Iterate through a series of lines with a patch applied.
447- This handles a single file, and does exact, not fuzzy patching.
448-
449- :param orig_lines: The unpatched lines.
450- :param hunks: An iterable of Hunk instances.
451- """
452- seen_patch = []
453- line_no = 1
454- if orig_lines is not None:
455- orig_lines = iter(orig_lines)
456- for hunk in hunks:
457- while line_no < hunk.orig_pos:
458- orig_line = orig_lines.next()
459- yield orig_line
460- line_no += 1
461- for hunk_line in hunk.lines:
462- seen_patch.append(str(hunk_line))
463- if isinstance(hunk_line, InsertLine):
464- yield hunk_line.contents
465- elif isinstance(hunk_line, (ContextLine, RemoveLine)):
466- orig_line = orig_lines.next()
467- if orig_line != hunk_line.contents:
468- raise PatchConflict(line_no, orig_line,
469- "".join(seen_patch))
470- if isinstance(hunk_line, ContextLine):
471- yield orig_line
472- else:
473- if not isinstance(hunk_line, RemoveLine):
474- raise AssertionError(hunk_line)
475- line_no += 1
476- if orig_lines is not None:
477- for line in orig_lines:
478- yield line