Merge lp:~spiv/bzr-builddeb/use-dpkg-mergechangelogs into lp:bzr-builddeb

Proposed by Andrew Bennetts
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 584
Merged at revision: 596
Proposed branch: lp:~spiv/bzr-builddeb/use-dpkg-mergechangelogs
Merge into: lp:bzr-builddeb
Diff against target: 438 lines (+167/-161)
4 files modified
README (+5/-0)
debian/changelog (+10/-1)
merge_changelog.py (+60/-116)
tests/test_merge_changelog.py (+92/-44)
To merge this branch: bzr merge lp:~spiv/bzr-builddeb/use-dpkg-mergechangelogs
Reviewer Review Type Date Requested Status
Jelmer Vernooij code Approve
Review via email: mp+68797@code.launchpad.net

Commit message

Use dpkg-mergechangelogs rather than custom logic to merge debian/changelog files.

Description of the change

This switches merge_changelog to simply call out to dpkg-mergechangelogs rather than use its own NIH logic.

As discussed on the bug and UDD list this required updating some tests for the new behaviour, but everyone seems confident that the new behaviour is actually better.

Fixing the tests revealed two very minor drawbacks to dpkg-mergechangelogs:

 1) when merging invalid changelogs it seems to lose the final line of text
 2) the conflict markers aren't quite standard: “<<<<<<” rather than “<<<<<<<” (6 vs. 7 chars).

1) is mitigated greatly by the fact that dpkg-mergechangelogs emits warnings when dealing with invalid changelogs, so developers will be encouraged to check and fix their data anyway. And I imagine invalid changelogs aren't too common, mostly people use tools to help them write them, and the format isn't *that* onerous.

2) is largely cosmetic. I can imagine it might impede using external tools to help deal with conflicts (like meld?) but with some luck those tools are tolerant of the difference, and that drawback is greatly outweighed by the benefit of doing better merges in the first place.

I've updated the README to list installing dpkg-dev as a requirement. For the intended audience of bzr-builddeb that seems pretty reasonable :)

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I think (1) is an acceptable regression given how many other regressions this mp fixes. We should probably file a bug against dpkg-dev about it.

For (2), would it be possible to do postprocessing on the file to fix up the conflict markers to be more standard? It seems like a minor thing though, so perhaps a bug report would also be nice for this.

Either way, I think this is a huge improvement over the previous situation.

[tweak]
Can you add the right entries for these bugs to debian/changelog? The dch command should make this easy ("dch 'Fix the foobar. LP: #...'").

review: Approve (code)
Revision history for this message
Andrew Bennetts (spiv) wrote :

Jelmer Vernooij wrote:
> I think (1) is an acceptable regression given how many other regressions this
> mp fixes. We should probably file a bug against dpkg-dev about it.

Yeah, I think so.

> For (2), would it be possible to do postprocessing on the file to fix up the
> conflict markers to be more standard? It seems like a minor thing though, so
> perhaps a bug report would also be nice for this.

Hmm! I guess we could! We know when dpkg-mergechangelogs thinks there's a
conflict, and I doubt those lines could appear in a valid changelog file. I'll
do that!

> [tweak]
> Can you add the right entries for these bugs to debian/changelog? The dch command should make this easy ("dch 'Fix the foobar. LP: #...'").

Ah, I wondered where the news file was, silly me :)

585. By Andrew Bennetts

Workaround bug 815700 in dpkg-mergechangelogs (it emits slightly non-standard conflict markers), and link to bug 815704 in an XXX comment in a test.

586. By Andrew Bennetts

Update debian/changelog.

Revision history for this message
Andrew Bennetts (spiv) wrote :

It turns out I'm not in the bzr-builddeb-hackers team so can't push directly to lp:bzr-builddeb.

Jelmer, could you push lp:~spiv/bzr-builddeb/trunk-merge-of-use-dpkg-mergechangelogs to lp:bzr-builddeb? (Unless someone else gets there first, of course!) It's just trunk plus a revision merging my changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README'
--- README 2011-01-10 21:59:19 +0000
+++ README 2011-07-25 08:06:14 +0000
@@ -24,6 +24,11 @@
2424
25.. _python-debian: http://bzr.debian.org/pkg-python-debian/trunk/25.. _python-debian: http://bzr.debian.org/pkg-python-debian/trunk/
2626
27It also requires the ``dpkg-dev`` package to be installed (for the
28``dpkg-mergechangelogs`` tool)::
29
30 apt-get install dpkg-dev
31
27This plugin can be installed in two ways. As you are probably using a Debian32This plugin can be installed in two ways. As you are probably using a Debian
28system you can probably just use the Debian packages. The other way is to 33system you can probably just use the Debian packages. The other way is to
29branch it in to ``~/.bazaar/plugins/builddeb``, i.e::34branch it in to ``~/.bazaar/plugins/builddeb``, i.e::
3035
=== modified file 'debian/changelog'
--- debian/changelog 2011-07-20 19:28:00 +0000
+++ debian/changelog 2011-07-25 08:06:14 +0000
@@ -1,9 +1,18 @@
1bzr-builddeb (2.7.7) UNRELEASED; urgency=low1bzr-builddeb (2.7.7) UNRELEASED; urgency=low
22
3 [ Jelmer Vernooij ]
3 * Build type now defaults to normal mode when used in an empty tree.4 * Build type now defaults to normal mode when used in an empty tree.
4 LP: #7765285 LP: #776528
56
6 -- Jelmer Vernooij <jelmer@debian.org> Wed, 20 Jul 2011 21:27:48 +02007 [ Andrew Bennetts ]
8 * Use dpkg-mergechangelogs(1) to merge changelog files. Fixes LP:
9 #718944 (preserve the existing ordering of changelog entries when
10 merging); LP: #552950 (Additional changelog entries are modified
11 when merging another branch); LP: #517093 (3-way changelog file
12 merge doesn't do textual merging on sections); and LP: #517090 (3-
13 way changelog file merge doesn't support deletions).
14
15 -- Andrew Bennetts <andrew@bemusement.org> Mon, 25 Jul 2011 16:38:09 +1000
716
8bzr-builddeb (2.7.6) unstable; urgency=low17bzr-builddeb (2.7.6) unstable; urgency=low
918
1019
=== modified file 'merge_changelog.py'
--- merge_changelog.py 2010-05-28 20:47:13 +0000
+++ merge_changelog.py 2011-07-25 08:06:14 +0000
@@ -16,13 +16,24 @@
16# You should have received a copy of the GNU General Public License16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.17# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19import logging
20import os.path
19import re21import re
22import shutil
23import subprocess
24import tempfile
2025
21from bzrlib import (26from bzrlib import (
22 merge,27 merge,
23 )28 )
2429
2530
31# A logger in the 'bzr' hierarchy. By default messages will be propagated to
32# the standard bzr logger, but tests can easily intercept just this logger if
33# they wish.
34_logger = logging.getLogger('bzr.plugins.builddeb.merge_changelog')
35
36
26class ChangeLogFileMerge(merge.ConfigurableFileMerger):37class ChangeLogFileMerge(merge.ConfigurableFileMerger):
2738
28 name_prefix = 'deb_changelog'39 name_prefix = 'deb_changelog'
@@ -30,123 +41,56 @@
3041
31 def merge_text(self, params):42 def merge_text(self, params):
32 return merge_changelog(params.this_lines, params.other_lines,43 return merge_changelog(params.this_lines, params.other_lines,
33 params.base_lines)44 params.base_lines)
3445
35
36# Regular expression for top of debian/changelog
37CL_RE = re.compile(r'^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)((\s+[-0-9a-z]+)+)\;',
38 re.IGNORECASE)
3946
40def merge_changelog(this_lines, other_lines, base_lines=[]):47def merge_changelog(this_lines, other_lines, base_lines=[]):
41 """Merge a changelog file."""48 """Merge a changelog file."""
42 try:49 # Write the BASE, THIS and OTHER versions to files in a temporary
43 from debian import changelog50 # directory, and use dpkg-mergechangelogs to merge them.
44 except ImportError:51 tmpdir = tempfile.mkdtemp('deb_changelog_merge')
45 # Prior to 0.1.15 the debian module was called debian_bundle52 try:
46 from debian_bundle import changelog53 def writelines(filename, lines):
4754 with open(filename, 'w') as f:
48 try:55 for line in lines:
49 left_cl = read_changelog(this_lines)56 f.write(line)
50 right_cl = read_changelog(other_lines)57 base_filename = os.path.join(tmpdir, 'changelog.base')
51 # BASE lines don't end up in the output, so we allow strict=False58 this_filename = os.path.join(tmpdir, 'changelog.this')
52 base_cl = read_changelog(base_lines, strict=False)59 other_filename = os.path.join(tmpdir, 'changelog.other')
53 except changelog.ChangelogParseError:60 writelines(base_filename, base_lines)
54 return ('not_applicable', None)61 writelines(this_filename, this_lines)
5562 writelines(other_filename, other_lines)
56 content = []63 proc = subprocess.Popen(['dpkg-mergechangelogs', base_filename,
57 def step(iterator):64 this_filename, other_filename], stdout=subprocess.PIPE,
58 try:65 stderr=subprocess.PIPE)
59 return iterator.next()66 stdout, stderr = proc.communicate()
60 except StopIteration:67 retcode = proc.returncode
61 return None68 if stderr:
62 left_blocks = dict((b.version, b) for b in left_cl._blocks)69 # Relay the warning from dpkg-mergechangelogs to the user. We
63 right_blocks = dict((b.version, b) for b in right_cl._blocks)70 # don't decorate the messages at all, as dpkg-mergechangelogs
64 # Unfortunately, while version objects implement __eq__ they *don't*71 # warnings are already prefixed with "dpkg-mergechangelogs:
65 # implement __hash__, which means we can't do dict lookups properly, so72 # warning:" which makes the origin of the messages quite clear.
66 # instead, we fall back on the version string instead of the object.73 _logger.warning('%s', stderr)
67 # Make sure never to try to use right_version in left_blocks because of74 if retcode == 1:
68 # this.75 # dpkg-mergechangelogs reports a conflict. Unfortunately it uses
69 # We lazily parse the base data, in case we never need it76 # slightly non-standard conflict markers (<http://pad.lv/815700>:
70 base_blocks = dict((b.version.full_version, b) for b in base_cl._blocks)77 # "<<<<<<" rather than "<<<<<<<", i.e. 6 chars instead of 7), so we
71 left_order = iter(sorted(left_blocks.keys(), reverse=True))78 # correct that here to make the results of this plugin as
72 right_order = iter(sorted(right_blocks.keys(), reverse=True))79 # consistent with regular bzr usage as possible. Note that
73 left_version = step(left_order)80 # conflict markers are never valid lines in a changelog file, so
74 right_version = step(right_order)81 # it's reasonable for us to assume that any line that looks like a
7582 # conflict marker is a conflict marker (rather than valid content).
76 # TODO: Do we want to support the ability to delete a section? We could do83 # At worst a conflicted merge of an invalid changelog file that
77 # a first-pass algorithm that checks the versions in base versus the84 # already contained a non-standard conflict marker will have that
78 # versions in this and other, to determine what versions should be in85 # conflict marker made standard, which is more like a feature than
79 # the output. For now, we just assume that if a version is present in86 # a bug!
80 # any of this or other, then we want it in the output.87 def replace_func(match_obj):
81 conflict_status = 'success'88 match_text = match_obj.group(0)
8289 return match_text[0] * 7
83 while left_version is not None or right_version is not None:90 stdout = re.sub(
84 if (left_version is None or91 '^[<=>]{6}$', replace_func, stdout, flags=re.MULTILINE)
85 (right_version is not None and right_version > left_version)):92 return 'conflicted', stdout
86 next_content = str(right_blocks[right_version])
87 right_version = step(right_order)
88 elif (right_version is None or
89 (left_version is not None and left_version > right_version)):
90 next_content = str(left_blocks[left_version])
91 left_version = step(left_order)
92 else:93 else:
93 assert left_version == right_version94 return 'success', stdout
94 # Same version, step both95 finally:
95 # TODO: Conflict if left_version != right96 shutil.rmtree(tmpdir)
96 # Note: See above comment why we can't use
97 # right_blocks[left_version] even though they *should* be
98 # equivalent
99 left_content = str(left_blocks[left_version])
100 right_content = str(right_blocks[right_version])
101 if left_content == right_content:
102 # Identical content
103 next_content = left_content
104 else:
105 # Sides disagree, compare with base
106 base_content = str(base_blocks.get(left_version.full_version,
107 ''))
108 if left_content == base_content:
109 next_content = right_content
110 elif right_content == base_content:
111 next_content = left_content
112 else:
113 # TODO: We could use merge3.Merge3 to try a line-based
114 # textual merge on the content. However, for now I'm
115 # just going to conflict on the whole region
116 # Conflict names taken from merge.py
117 next_content = ('<<<<<<< TREE\n'
118 + left_content
119 + '=======\n'
120 + right_content
121 + '>>>>>>> MERGE-SOURCE\n'
122 )
123 conflict_status = 'conflicted'
124 next_block = left_blocks[left_version]
125 left_version = step(left_order)
126 right_version = step(right_order)
127 content.append(next_content)
128
129 return conflict_status, content
130
131
132def read_changelog(lines, strict=True):
133 """Return a parsed changelog file."""
134 try:
135 from debian import changelog
136 except ImportError:
137 # Prior to 0.1.15 the debian module was called debian_bundle
138 from debian_bundle import changelog
139 # Note: There appears to be a bug in Changelog if you pass it an iterable
140 # of lines (like a file obj, or a list of lines). Specifically, it
141 # does not strip trailing newlines, and it adds ones back in, so you
142 # get doubled blank lines... :(
143 # So we just ''.join() the lines and don't worry about it
144 # Note: There is also a bug that the Changelog constructor suppresses parse
145 # errors, so we want to always call parse_changelog separately
146 content = ''.join(lines)
147 cl = changelog.Changelog()
148 if content:
149 # We get a warning if we try to parse an empty changelog file, which in
150 # strict mode is an error, so only parse when we have content
151 cl.parse_changelog(content, strict=strict)
152 return cl
15397
=== modified file 'tests/test_merge_changelog.py'
--- tests/test_merge_changelog.py 2010-05-23 15:29:38 +0000
+++ tests/test_merge_changelog.py 2011-07-25 08:06:14 +0000
@@ -19,7 +19,7 @@
1919
20"""Tests for the merge_changelog code."""20"""Tests for the merge_changelog code."""
2121
22import warnings22import logging
2323
24try:24try:
25 from debian import changelog25 from debian import changelog
@@ -27,6 +27,9 @@
27 # Prior to 0.1.15 the debian module was called debian_bundle27 # Prior to 0.1.15 the debian module was called debian_bundle
28 from debian_bundle import changelog28 from debian_bundle import changelog
2929
30from testtools.content_type import ContentType
31from testtools.content import Content
32
30from bzrlib import (33from bzrlib import (
31 memorytree,34 memorytree,
32 merge,35 merge,
@@ -70,6 +73,38 @@
70""".splitlines(True)73""".splitlines(True)
7174
7275
76# Merge of 2b and 2c using 2 as the base (b adds a line, c adds a line and
77# deletes a line).
78v_111_2bc = """\
79psuedo-prog (1.1.1-2) unstable; urgency=low
80
81 * New upstream release.
82 * Yet another content for 1.1.1-2
83 * But more is better
84
85 -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
86
87""".splitlines(True)
88
89
90# Merge of 2b and 2c using an empty base. (As calculated by
91# dpkg-mergechangelogs.)
92v_111_2bc_empty_base = """\
93psuedo-prog (1.1.1-2) unstable; urgency=low
94
95 * New upstream release.
96<<<<<<<
97 * Awesome bug fixes.
98=======
99 * Yet another content for 1.1.1-2
100>>>>>>>
101 * But more is better
102
103 -- Joe Foo <joe@example.com> Thu, 28 Jan 2010 10:45:44 +0000
104
105""".splitlines(True)
106
107
73v_112_1 = """\108v_112_1 = """\
74psuedo-prog (1.1.2-1) unstable; urgency=low109psuedo-prog (1.1.2-1) unstable; urgency=low
75110
@@ -92,15 +127,37 @@
92""".splitlines(True)127""".splitlines(True)
93128
94129
95class TestReadChangelog(tests.TestCase):130# Backports from current testtools so that we remain compatible with testtools
131# 0.9.2 (the version in lucid).
132UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'})
133def text_content(text):
134 """Create a `Content` object from some text.
96135
97 def test_read_changelog(self):136 This is useful for adding details which are short strings.
98 cl = merge_changelog.read_changelog(v_112_1)137 """
99 self.assertEqual(1, len(cl._blocks))138 return Content(UTF8_TEXT, lambda: [text.encode('utf8')])
100139
101140
102class TestMergeChangelog(tests.TestCase):141class TestMergeChangelog(tests.TestCase):
103142
143 def setUp(self):
144 super(tests.TestCase, self).setUp()
145 # Intercept warnings from merge_changelog's logger: this is where
146 self.logged_warnings = self.make_utf8_encoded_stringio()
147 self.addCleanup(self.addMergeChangelogWarningsDetail)
148 handler = logging.StreamHandler(self.logged_warnings)
149 handler.setLevel(logging.WARNING)
150 logger = logging.getLogger('bzr.plugins.builddeb.merge_changelog')
151 logger.addHandler(handler)
152 self.addCleanup(logger.removeHandler, handler)
153 self.overrideAttr(logger, 'propagate', False)
154
155 def addMergeChangelogWarningsDetail(self):
156 warnings_log = self.logged_warnings.getvalue()
157 if warnings_log:
158 self.addDetail(
159 'merge_changelog warnings', text_content(warnings_log))
160
104 def assertMergeChangelog(self, expected_lines, this_lines, other_lines,161 def assertMergeChangelog(self, expected_lines, this_lines, other_lines,
105 base_lines=[], conflicted=False):162 base_lines=[], conflicted=False):
106 status, merged_lines = merge_changelog.merge_changelog(163 status, merged_lines = merge_changelog.merge_changelog(
@@ -139,10 +196,10 @@
139 base_lines=[])196 base_lines=[])
140197
141 def test_unsorted(self):198 def test_unsorted(self):
142 # Passing in an improperly sorted text should result in a properly199 # The order of entries being merged is unchanged, even if they are not
143 # sorted one200 # properly sorted. (This is a merge tool, not a reformatting tool.)
144 self.assertMergeChangelog(v_111_2 + v_001_1,201 self.assertMergeChangelog(v_111_2 + v_001_1,
145 this_lines = v_001_1 + v_111_2,202 this_lines = v_111_2 + v_001_1,
146 other_lines = [],203 other_lines = [],
147 base_lines = [])204 base_lines = [])
148205
@@ -158,20 +215,11 @@
158215
159 def test_3way_conflicted(self):216 def test_3way_conflicted(self):
160 self.assertMergeChangelog(217 self.assertMergeChangelog(
161 expected_lines=['<<<<<<< TREE\n']218 expected_lines=v_111_2bc,
162 + v_111_2b
163 + ['=======\n']
164 + v_111_2c
165 + ['>>>>>>> MERGE-SOURCE\n'],
166 this_lines=v_111_2b, other_lines=v_111_2c,219 this_lines=v_111_2b, other_lines=v_111_2c,
167 base_lines=v_111_2,220 base_lines=v_111_2)
168 conflicted=True)
169 self.assertMergeChangelog(221 self.assertMergeChangelog(
170 expected_lines=['<<<<<<< TREE\n']222 expected_lines=v_111_2bc_empty_base,
171 + v_111_2b
172 + ['=======\n']
173 + v_111_2c
174 + ['>>>>>>> MERGE-SOURCE\n'],
175 this_lines=v_111_2b, other_lines=v_111_2c,223 this_lines=v_111_2b, other_lines=v_111_2c,
176 base_lines=[],224 base_lines=[],
177 conflicted=True)225 conflicted=True)
@@ -186,30 +234,30 @@
186 -- Thu, 28 Jan 2010 10:45:44 +0000234 -- Thu, 28 Jan 2010 10:45:44 +0000
187235
188""".splitlines(True)236""".splitlines(True)
189 # Missing the author and we don't have allow_missing_author set237 # invalid_changelog is missing the author, but dpkg-mergechangelogs
190 cl = changelog.Changelog()238 # copes gracefully with invalid input.
191 self.assertRaises(changelog.ChangelogParseError,239 status, lines = merge_changelog.merge_changelog(
192 cl.parse_changelog, ''.join(invalid_changelog), strict=True)240 invalid_changelog, v_111_2, v_111_2)
193 # If strict parsing fails, don't try to do special merging241 self.assertEqual('success', status)
194 self.assertEqual(('not_applicable', None),242 # XXX: ideally we'd expect ''.join(lines) ==
195 merge_changelog.merge_changelog(invalid_changelog, v_111_2,243 # ''.join(invalid_changelog), but dpkg-mergechangelogs appears to lose
196 v_111_2))244 # the final line in these examples.
197 self.assertEqual(('not_applicable', None),245 # <https://bugs.launchpad.net/ubuntu/+source/dpkg/+bug/815704>
198 merge_changelog.merge_changelog(v_111_2, invalid_changelog,246 # - Andrew Bennetts, 25 July 2011.
199 v_111_2))247 #self.assertEqual(''.join(invalid_changelog), ''.join(lines))
200 # We are non-strict about parsing BASE, because its contents are not248 status, lines = merge_changelog.merge_changelog(
201 # included in the output.249 invalid_changelog, v_111_2, v_111_2)
202 # This triggers a warning, but we don't want to clutter the test run250 self.assertEqual('success', status)
203 cur_filters = warnings.filters[:]251 #self.assertEqual(''.join(invalid_changelog), ''.join(lines))
204 warnings.simplefilter('ignore', UserWarning)252 self.assertMergeChangelog(v_112_1 +
205 try:253 ['<<<<<<<\n'] +
206 self.assertMergeChangelog(v_112_1 + v_111_2,254 v_111_2 +
207 this_lines=v_111_2,255 ['=======\n>>>>>>>\n'],
208 other_lines=v_112_1,256 this_lines=v_111_2,
209 base_lines=invalid_changelog,257 other_lines=v_112_1,
210 )258 base_lines=invalid_changelog,
211 finally:259 conflicted=True
212 warnings.filters = cur_filters[:]260 )
213261
214262
215class TestChangelogHook(tests.TestCaseWithMemoryTransport):263class TestChangelogHook(tests.TestCaseWithMemoryTransport):

Subscribers

People subscribed via source and target branches