Merge lp:~jameinel/bzr-builddeb/changelog-parser into lp:bzr-builddeb

Proposed by John A Meinel on 2010-02-03
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jameinel/bzr-builddeb/changelog-parser
Merge into: lp:bzr-builddeb
Diff against target: 609 lines (+271/-269)
2 files modified
merge_changelog.py (+104/-222)
tests/test_merge_changelog.py (+167/-47)
To merge this branch: bzr merge lp:~jameinel/bzr-builddeb/changelog-parser
Reviewer Review Type Date Requested Status
Robert Collins (community) 2010-02-03 Approve on 2010-02-04
Review via email: mp+18557@code.launchpad.net
To post a comment you must log in.
John A Meinel (jameinel) wrote :

This removes the guts of the Changelog parsing code that I brought in, and replaces it by just importing debian_bundle.changelog and having that do the parsing.

I don't really like depending on cl._blocks, but since import_dsc is doing so, I assumed that was the best we could do.

The tests all still pass, and the parsing is going to be as accurate as python-debian's parsing, which is what import_dsc uses anyway.

402. By John A Meinel on 2010-02-03

Make sure that the blocks are in sorted order before we do anything else.

403. By John A Meinel on 2010-02-03

Fix bug #516060, implement 3-way changelog merge.

This uses a fairly crude 3-way changelog merge algorithm, but doesn't yet
introduce conflicts. That will be next, as it requires an api bump.
If left != right, then we check if the version is in base, and if
one side is identical to base, then we pick the other.
This doesn't:
a) Conflict when left != right != base
b) Allow deleting entries (present in base & this but not other, for instance)
etc.

404. By John A Meinel on 2010-02-03

Conflict when appropriate.

We could try to do a textual merge, but it is just easier to
conflict on the whole region. Mostly because merge3.Merge3 is missing
a decent api for telling whether there was a conflict region :(.

John A Meinel (jameinel) wrote :

I just updated this branch. I was going to submit a separate proposal, but I did find a bug here, and just fixed it while doing the rest.

Anyway, this now does 3-way merging of the blocks. The basic logic is:

1) Find all blocks in THIS, OTHER and BASE
2) For all blocks in THIS and OTHER, include them in the output in sorted order.
3) If THIS and OTHER both contain a block (matched by Version) with different content, compare against the BASE content
  a) If THIS == BASE, chose OTHER
  b) if OTHER == BASE, chose THIS
  c) If BASE is not present, or BASE is different from both, conflict

What this is missing:

1) If THIS or OTHER deletes a block it will be restored. (I assume that if a version is in THIS or OTHER then you always want it in the output)
2) 3c could use merge3.Merge3() to try to do a textual merge of the conflicted region.

I can open bugs on those two bits if you want.

405. By John A Meinel on 2010-02-03

Simplify the conflict logic slightly.

Robert Collins (lifeless) wrote :

On Wed, 2010-02-03 at 22:19 +0000, John A Meinel wrote:
>
> What this is missing:
>
> 1) If THIS or OTHER deletes a block it will be restored. (I assume
> that if a version is in THIS or OTHER then you always want it in the
> output)
> 2) 3c could use merge3.Merge3() to try to do a textual merge of the
> conflicted region.
>
> I can open bugs on those two bits if you want.
--

please do

-Rob

406. By John A Meinel on 2010-02-03

Trying to trigger the invalid code, only to find bugs in python-debian :(

Robert Collins (lifeless) wrote :

 review: approve

On Wed, 2010-02-03 at 20:19 +0000, John A Meinel wrote:
>
> + elif right_block is None:
> + next_block = left_block
> + left_block = step(left_blocks)
> ...

> + elif left_block.version > right_block.version:
> + # left comes first
> + next_block = left_block
> + left_block = step(left_blocks)

The None and > cases are the same - I think it would be shorter to group
them.

elif right_block_is None or left_block.version > right_block.version:

-Rob

review: Approve
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Robert Collins wrote:
> Review: Approve
> review: approve
>
> On Wed, 2010-02-03 at 20:19 +0000, John A Meinel wrote:
>> + elif right_block is None:
>> + next_block = left_block
>> + left_block = step(left_blocks)
>> ...
>
>> + elif left_block.version > right_block.version:
>> + # left comes first
>> + next_block = left_block
>> + left_block = step(left_blocks)
>
>
> The None and > cases are the same - I think it would be shorter to group
> them.
>
> elif right_block_is None or left_block.version > right_block.version:
>
> -Rob
>
>

I believe I've updated to that style in more recent patches. But I'm
also trying to address some of James's concerns about always being
active. I'm trying to get Strict handling and fallback to regular
processing working (only to find that Strict isn't properly triggering
in some edge cases... :(

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktq63wACgkQJdeBCYSNAAOBmwCg1EmjxckXoDq5QlGLAln6P9Rz
50gAn02X+OR5lx/dWL/QmrAfSCtr4f7r
=271q
-----END PGP SIGNATURE-----

407. By John A Meinel on 2010-02-04

Sort out some more details for the 3-way merge code.
The main problem I was running into was that the constructor always suppressed parse failures.

James Westby (james-w) wrote :

I think what I would like to do is use strict=True and then let
merge3 do the merge if we can't parse it. That way we won't drop
text if we can't parse it.

Thanks,

James

James Westby (james-w) wrote :

Oh, it already has code for that, but I don't understand the comment

 # BASE lines don't end up in the output, so we allow strict=False

Would it also be good to note to the user when we tried to merge but
it failed due to parsing?

Thanks,

James

John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

James Westby wrote:
> I think what I would like to do is use strict=True and then let
> merge3 do the merge if we can't parse it. That way we won't drop
> text if we can't parse it.
>
> Thanks,
>
> James
>

That is what I ended up with. I don't know if the current diff shows that.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkt1na4ACgkQJdeBCYSNAAMTQgCfYfcKCebNDyQPtpkwkCrInf0S
Bh0Anit1wIn+m7J24Gr7U1p/sBsfQsQS
=h8Ig
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'merge_changelog.py'
2--- merge_changelog.py 2010-01-29 10:51:45 +0000
3+++ merge_changelog.py 2010-02-04 16:34:14 +0000
4@@ -20,243 +20,125 @@
5
6 from bzrlib import (
7 merge,
8+ merge3,
9 )
10
11+from debian_bundle import changelog
12+
13 class ChangeLogFileMerge(merge.ConfigurableFileMerger):
14
15 name_prefix = 'deb_changelog'
16 default_files = ['debian/changelog']
17
18 def merge_text(self, params):
19- return 'success', merge_changelog(params.this_lines, params.other_lines)
20-
21-
22-########################################################################
23-# Changelog Management
24-########################################################################
25+ return merge_changelog(params.this_lines, params.other_lines,
26+ params.base_lines)
27+
28
29 # Regular expression for top of debian/changelog
30 CL_RE = re.compile(r'^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)((\s+[-0-9a-z]+)+)\;',
31 re.IGNORECASE)
32
33-def merge_changelog(left_changelog_lines, right_changelog_lines):
34+def merge_changelog(this_lines, other_lines, base_lines=[]):
35 """Merge a changelog file."""
36
37- left_cl = read_changelog(left_changelog_lines)
38- right_cl = read_changelog(right_changelog_lines)
39+ try:
40+ left_cl = read_changelog(this_lines)
41+ right_cl = read_changelog(other_lines)
42+ # BASE lines don't end up in the output, so we allow strict=False
43+ base_cl = read_changelog(base_lines, strict=False)
44+ except changelog.ChangelogParseError:
45+ return ('not_applicable', None)
46
47 content = []
48- # TODO: This is not a 3-way merge, but a 2-way merge
49- # The resolution is currently 'if left and right have texts that have
50- # the same "version" string, use left', aka "prefer-mine".
51- # We could introduce BASE, and cause conflicts, or appropriately
52- # resolve, etc.
53- # Note also that this code is only invoked when there is a
54- # left-and-right change, so merging a pure-right change will take all
55- # changes.
56- for right_ver, right_text in right_cl:
57- while len(left_cl) and left_cl[0][0] > right_ver:
58- (left_ver, left_text) = left_cl.pop(0)
59- content.append(left_text)
60- content.append('\n')
61-
62- while len(left_cl) and left_cl[0][0] == right_ver:
63- (left_ver, left_text) = left_cl.pop(0)
64-
65- content.append(right_text)
66- content.append('\n')
67-
68- for left_ver, left_text in left_cl:
69- content.append(left_text)
70- content.append('\n')
71-
72- return content
73-
74-
75-def read_changelog(lines):
76+ def step(iterator):
77+ try:
78+ return iterator.next()
79+ except StopIteration:
80+ return None
81+ left_blocks = dict((b.version, b) for b in left_cl._blocks)
82+ right_blocks = dict((b.version, b) for b in right_cl._blocks)
83+ # Unfortunately, while version objects implement __eq__ they *don't*
84+ # implement __hash__, which means we can't do dict lookups properly, so
85+ # instead, we fall back on the version string instead of the object.
86+ # Make sure never to try to use right_version in left_blocks because of
87+ # this.
88+ # We lazily parse the base data, in case we never need it
89+ base_blocks = dict((b.version.full_version, b) for b in base_cl._blocks)
90+ left_order = iter(sorted(left_blocks.keys(), reverse=True))
91+ right_order = iter(sorted(right_blocks.keys(), reverse=True))
92+ left_version = step(left_order)
93+ right_version = step(right_order)
94+
95+ # TODO: Do we want to support the ability to delete a section? We could do
96+ # a first-pass algorithm that checks the versions in base versus the
97+ # versions in this and other, to determine what versions should be in
98+ # the output. For now, we just assume that if a version is present in
99+ # any of this or other, then we want it in the output.
100+ conflict_status = 'success'
101+
102+ while left_version is not None or right_version is not None:
103+ if (left_version is None or
104+ (right_version is not None and right_version > left_version)):
105+ next_content = str(right_blocks[right_version])
106+ right_version = step(right_order)
107+ elif (right_version is None or
108+ (left_version is not None and left_version > right_version)):
109+ next_content = str(left_blocks[left_version])
110+ left_version = step(left_order)
111+ else:
112+ assert left_version == right_version
113+ # Same version, step both
114+ # TODO: Conflict if left_version != right
115+ # Note: See above comment why we can't use
116+ # right_blocks[left_version] even though they *should* be
117+ # equivalent
118+ left_content = str(left_blocks[left_version])
119+ right_content = str(right_blocks[right_version])
120+ if left_content == right_content:
121+ # Identical content
122+ next_content = left_content
123+ else:
124+ # Sides disagree, compare with base
125+ base_content = str(base_blocks.get(left_version.full_version,
126+ ''))
127+ if left_content == base_content:
128+ next_content = right_content
129+ elif right_content == base_content:
130+ next_content = left_content
131+ else:
132+ # TODO: We could use merge3.Merge3 to try a line-based
133+ # textual merge on the content. However, for now I'm
134+ # just going to conflict on the whole region
135+ # Conflict names taken from merge.py
136+ next_content = ('<<<<<<< TREE\n'
137+ + left_content
138+ + '=======\n'
139+ + right_content
140+ + '>>>>>>> MERGE-SOURCE\n'
141+ )
142+ conflict_status = 'conflicted'
143+ next_block = left_blocks[left_version]
144+ left_version = step(left_order)
145+ right_version = step(right_order)
146+ content.append(next_content)
147+
148+ return conflict_status, content
149+
150+
151+def read_changelog(lines, strict=True):
152 """Return a parsed changelog file."""
153- entries = []
154-
155- (ver, text) = (None, "")
156- for line in lines:
157- match = CL_RE.search(line)
158- if match:
159- try:
160- ver = Version(match.group(2))
161- except ValueError:
162- ver = None
163-
164- text += line
165- elif line.startswith(" -- "):
166- if ver is None:
167- ver = Version("0")
168-
169- text += line
170- entries.append((ver, text))
171- (ver, text) = (None, "")
172- elif len(line.strip()) or ver is not None:
173- text += line
174-
175- if len(text):
176- entries.append((ver, text))
177-
178- return entries
179-
180-########################################################################
181-# Version parsing code
182-########################################################################
183-# Regular expressions make validating things easy
184-valid_epoch = re.compile(r'^[0-9]+$')
185-valid_upstream = re.compile(r'^[A-Za-z0-9+:.~-]*$')
186-valid_revision = re.compile(r'^[A-Za-z0-9+.~]+$')
187-
188-# Character comparison table for upstream and revision components
189-cmp_table = "~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:"
190-
191-
192-class Version(object):
193- """Debian version number.
194-
195- This class is designed to be reasonably transparent and allow you
196- to write code like:
197-
198- | s.version >= '1.100-1'
199-
200- The comparison will be done according to Debian rules, so '1.2' will
201- compare lower.
202-
203- Properties:
204- epoch Epoch
205- upstream Upstream version
206- revision Debian/local revision
207- """
208-
209- def __init__(self, ver):
210- """Parse a string or number into the three components."""
211- self.epoch = 0
212- self.upstream = None
213- self.revision = None
214-
215- ver = str(ver)
216- if not len(ver):
217- raise ValueError
218-
219- # Epoch is component before first colon
220- idx = ver.find(":")
221- if idx != -1:
222- self.epoch = ver[:idx]
223- if not len(self.epoch):
224- raise ValueError
225- if not valid_epoch.search(self.epoch):
226- raise ValueError
227- ver = ver[idx+1:]
228-
229- # Revision is component after last hyphen
230- idx = ver.rfind("-")
231- if idx != -1:
232- self.revision = ver[idx+1:]
233- if not len(self.revision):
234- raise ValueError
235- if not valid_revision.search(self.revision):
236- raise ValueError
237- ver = ver[:idx]
238-
239- # Remaining component is upstream
240- self.upstream = ver
241- if not len(self.upstream):
242- raise ValueError
243- if not valid_upstream.search(self.upstream):
244- raise ValueError
245-
246- self.epoch = int(self.epoch)
247-
248- def getWithoutEpoch(self):
249- """Return the version without the epoch."""
250- str = self.upstream
251- if self.revision is not None:
252- str += "-%s" % (self.revision,)
253- return str
254-
255- without_epoch = property(getWithoutEpoch)
256-
257- def __str__(self):
258- """Return the class as a string for printing."""
259- str = ""
260- if self.epoch > 0:
261- str += "%d:" % (self.epoch,)
262- str += self.upstream
263- if self.revision is not None:
264- str += "-%s" % (self.revision,)
265- return str
266-
267- def __repr__(self):
268- """Return a debugging representation of the object."""
269- return "<%s epoch: %d, upstream: %r, revision: %r>" \
270- % (self.__class__.__name__, self.epoch,
271- self.upstream, self.revision)
272-
273- def __cmp__(self, other):
274- """Compare two Version classes."""
275- other = Version(other)
276-
277- result = cmp(self.epoch, other.epoch)
278- if result != 0: return result
279-
280- result = deb_cmp(self.upstream, other.upstream)
281- if result != 0: return result
282-
283- result = deb_cmp(self.revision or "", other.revision or "")
284- if result != 0: return result
285-
286- return 0
287-
288-
289-def strcut(str, idx, accept):
290- """Cut characters from str that are entirely in accept."""
291- ret = ""
292- while idx < len(str) and str[idx] in accept:
293- ret += str[idx]
294- idx += 1
295-
296- return (ret, idx)
297-
298-def deb_order(str, idx):
299- """Return the comparison order of two characters."""
300- if idx >= len(str):
301- return 0
302- elif str[idx] == "~":
303- return -1
304- else:
305- return cmp_table.index(str[idx])
306-
307-def deb_cmp_str(x, y):
308- """Compare two strings in a deb version."""
309- idx = 0
310- while (idx < len(x)) or (idx < len(y)):
311- result = deb_order(x, idx) - deb_order(y, idx)
312- if result < 0:
313- return -1
314- elif result > 0:
315- return 1
316-
317- idx += 1
318-
319- return 0
320-
321-def deb_cmp(x, y):
322- """Implement the string comparison outlined by Debian policy."""
323- x_idx = y_idx = 0
324- while x_idx < len(x) or y_idx < len(y):
325- # Compare strings
326- (x_str, x_idx) = strcut(x, x_idx, cmp_table)
327- (y_str, y_idx) = strcut(y, y_idx, cmp_table)
328- result = deb_cmp_str(x_str, y_str)
329- if result != 0: return result
330-
331- # Compare numbers
332- (x_str, x_idx) = strcut(x, x_idx, "0123456789")
333- (y_str, y_idx) = strcut(y, y_idx, "0123456789")
334- result = cmp(int(x_str or "0"), int(y_str or "0"))
335- if result != 0: return result
336-
337- return 0
338+ # Note: There appears to be a bug in Changelog if you pass it an iterable
339+ # of lines (like a file obj, or a list of lines). Specifically, it
340+ # does not strip trailing newlines, and it adds ones back in, so you
341+ # get doubled blank lines... :(
342+ # So we just ''.join() the lines and don't worry about it
343+ # Note: There is also a bug that the Changelog constructor suppresses parse
344+ # errors, so we want to always call parse_changelog separately
345+ content = ''.join(lines)
346+ cl = changelog.Changelog()
347+ if content:
348+ # We get a warning if we try to parse an empty changelog file, which in
349+ # strict mode is an error, so only parse when we have content
350+ cl.parse_changelog(content, strict=strict)
351+ return cl
352
353=== modified file 'tests/test_merge_changelog.py'
354--- tests/test_merge_changelog.py 2010-01-29 10:51:45 +0000
355+++ tests/test_merge_changelog.py 2010-02-04 16:34:14 +0000
356@@ -1,5 +1,5 @@
357 # Copyright (C) 2010 Canonical Ltd
358-#
359+#
360 # This file is part of bzr-builddeb.
361 #
362 # bzr-builddeb is free software; you can redistribute it and/or modify
363@@ -19,6 +19,10 @@
364
365 """Tests for the merge_changelog code."""
366
367+import warnings
368+
369+from debian_bundle import changelog
370+
371 from bzrlib import (
372 memorytree,
373 merge,
374@@ -28,66 +32,181 @@
375 from bzrlib.plugins.builddeb import merge_changelog
376
377
378+v_111_2 = """\
379+psuedo-prog (1.1.1-2) unstable; urgency=low
380+
381+ * New upstream release.
382+ * Awesome bug fixes.
383+
384+ -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
385+
386+""".splitlines(True)
387+
388+
389+v_111_2b = """\
390+psuedo-prog (1.1.1-2) unstable; urgency=low
391+
392+ * New upstream release.
393+ * Awesome bug fixes.
394+ * But more is better
395+
396+ -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
397+
398+""".splitlines(True)
399+
400+
401+v_111_2c = """\
402+psuedo-prog (1.1.1-2) unstable; urgency=low
403+
404+ * New upstream release.
405+ * Yet another content for 1.1.1-2
406+
407+ -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
408+
409+""".splitlines(True)
410+
411+
412+v_112_1 = """\
413+psuedo-prog (1.1.2-1) unstable; urgency=low
414+
415+ * New upstream release.
416+ * No bug fixes :(
417+
418+ -- Barry Foo <barry@example.com> Thu, 27 Jan 2010 10:45:44 +0000
419+
420+""".splitlines(True)
421+
422+
423+v_001_1 = """\
424+psuedo-prog (0.0.1-1) unstable; urgency=low
425+
426+ * New project released!!!!
427+ * No bugs evar
428+
429+ -- Barry Foo <barry@example.com> Thu, 27 Jan 2010 10:00:44 +0000
430+
431+""".splitlines(True)
432+
433+
434 class TestReadChangelog(tests.TestCase):
435
436 def test_read_changelog(self):
437- lines = """\
438-psuedo-prog (1.1.1-2) unstable; urgency=low
439-
440- * New upstream release.
441- * Awesome bug fixes.
442-
443- -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
444-""".splitlines(True)
445-
446-
447- entries = merge_changelog.read_changelog(lines)
448- self.assertEqual(1, len(entries))
449-
450-
451+ cl = merge_changelog.read_changelog(v_112_1)
452+ self.assertEqual(1, len(cl._blocks))
453+
454+
455 class TestMergeChangelog(tests.TestCase):
456
457- def assertMergeChangelog(self, expected_lines, this_lines, other_lines):
458- merged_lines = merge_changelog.merge_changelog(this_lines, other_lines)
459+ def assertMergeChangelog(self, expected_lines, this_lines, other_lines,
460+ base_lines=[], conflicted=False):
461+ status, merged_lines = merge_changelog.merge_changelog(
462+ this_lines, other_lines, base_lines)
463+ if conflicted:
464+ self.assertEqual('conflicted', status)
465+ else:
466+ self.assertEqual('success', status)
467 self.assertEqualDiff(''.join(expected_lines), ''.join(merged_lines))
468
469 def test_merge_by_version(self):
470- v_111_2 = """\
471-psuedo-prog (1.1.1-2) unstable; urgency=low
472-
473- * New upstream release.
474- * Awesome bug fixes.
475-
476- -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
477-
478-""".splitlines(True)
479-
480- v_112_1 = """\
481-psuedo-prog (1.1.2-1) unstable; urgency=low
482-
483- * New upstream release.
484- * No bug fixes :(
485-
486- -- Barry Foo <barry@example.com> Thu, 27 Jan 2010 10:45:44 +0000
487-
488-""".splitlines(True)
489-
490- v_001_1 = """\
491-psuedo-prog (0.0.1-1) unstable; urgency=low
492-
493- * New project released!!!!
494- * No bugs evar
495-
496- -- Barry Foo <barry@example.com> Thu, 27 Jan 2010 10:00:44 +0000
497-
498-""".splitlines(True)
499-
500 this_lines = v_111_2 + v_001_1
501 other_lines = v_112_1 + v_001_1
502 expected_lines = v_112_1 + v_111_2 + v_001_1
503 self.assertMergeChangelog(expected_lines, this_lines, other_lines)
504 self.assertMergeChangelog(expected_lines, other_lines, this_lines)
505
506+ def test_this_shorter(self):
507+ self.assertMergeChangelog(v_112_1 + v_111_2 + v_001_1,
508+ this_lines=v_111_2,
509+ other_lines=v_112_1 + v_001_1,
510+ base_lines=[])
511+ self.assertMergeChangelog(v_112_1 + v_111_2 + v_001_1,
512+ this_lines=v_001_1,
513+ other_lines=v_112_1 + v_111_2,
514+ base_lines=[])
515+
516+ def test_other_shorter(self):
517+ self.assertMergeChangelog(v_112_1 + v_111_2 + v_001_1,
518+ this_lines=v_112_1 + v_001_1,
519+ other_lines=v_111_2,
520+ base_lines=[])
521+ self.assertMergeChangelog(v_112_1 + v_111_2 + v_001_1,
522+ this_lines=v_112_1 + v_111_2,
523+ other_lines=v_001_1,
524+ base_lines=[])
525+
526+ def test_unsorted(self):
527+ # Passing in an improperly sorted text should result in a properly
528+ # sorted one
529+ self.assertMergeChangelog(v_111_2 + v_001_1,
530+ this_lines = v_001_1 + v_111_2,
531+ other_lines = [],
532+ base_lines = [])
533+
534+ def test_3way_merge(self):
535+ # Check that if one of THIS or OTHER matches BASE, then we select the
536+ # other content
537+ self.assertMergeChangelog(expected_lines=v_111_2,
538+ this_lines=v_111_2, other_lines=v_111_2b,
539+ base_lines=v_111_2b)
540+ self.assertMergeChangelog(expected_lines=v_111_2b,
541+ this_lines=v_111_2, other_lines=v_111_2b,
542+ base_lines=v_111_2)
543+
544+ def test_3way_conflicted(self):
545+ self.assertMergeChangelog(
546+ expected_lines=['<<<<<<< TREE\n']
547+ + v_111_2b
548+ + ['=======\n']
549+ + v_111_2c
550+ + ['>>>>>>> MERGE-SOURCE\n'],
551+ this_lines=v_111_2b, other_lines=v_111_2c,
552+ base_lines=v_111_2,
553+ conflicted=True)
554+ self.assertMergeChangelog(
555+ expected_lines=['<<<<<<< TREE\n']
556+ + v_111_2b
557+ + ['=======\n']
558+ + v_111_2c
559+ + ['>>>>>>> MERGE-SOURCE\n'],
560+ this_lines=v_111_2b, other_lines=v_111_2c,
561+ base_lines=[],
562+ conflicted=True)
563+
564+ def test_not_valid_changelog(self):
565+ invalid_changelog = """\
566+psuedo-prog (1.1.1-2) unstable; urgency=low
567+
568+ * New upstream release.
569+ * Awesome bug fixes.
570+
571+ -- Thu, 28 Jan 2010 10:45:44 +0000
572+
573+""".splitlines(True)
574+ # Missing the author and we don't have allow_missing_author set
575+ cl = changelog.Changelog()
576+ self.assertRaises(changelog.ChangelogParseError,
577+ cl.parse_changelog, ''.join(invalid_changelog), strict=True)
578+ # If strict parsing fails, don't try to do special merging
579+ self.assertEqual(('not_applicable', None),
580+ merge_changelog.merge_changelog(invalid_changelog, v_111_2,
581+ v_111_2))
582+ self.assertEqual(('not_applicable', None),
583+ merge_changelog.merge_changelog(v_111_2, invalid_changelog,
584+ v_111_2))
585+ # We are non-strict about parsing BASE, because its contents are not
586+ # included in the output.
587+ # This triggers a warning, but we don't want to clutter the test run
588+ cur_filters = warnings.filters[:]
589+ warnings.simplefilter('ignore', UserWarning)
590+ try:
591+ self.assertMergeChangelog(v_112_1 + v_111_2,
592+ this_lines=v_111_2,
593+ other_lines=v_112_1,
594+ base_lines=invalid_changelog,
595+ )
596+ finally:
597+ warnings.filters = cur_filters[:]
598+
599
600 class TestChangelogHook(tests.TestCaseWithMemoryTransport):
601
602@@ -122,6 +241,7 @@
603 def test_changelog_merge_hook_successful(self):
604 params, merger = self.make_params()
605 params.other_lines = ['']
606+ params.base_lines = ['']
607 file_merger = builddeb.changelog_merge_hook_factory(merger)
608 result, new_content = file_merger.merge_text(params)
609 self.assertEqual('success', result)

Subscribers

People subscribed via source and target branches