-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 The attached patch updates 2 bits to bzr-gtk for handling commit messages and per-file commit messages.
1) When committing, normalize line endings (\r => \n, \r\n => \n) 2) When reading a revision, if the file-info record fails to decode, handle it gracefully. John =:-> -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (Cygwin) Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org iEYEARECAAYFAkkb0cwACgkQJdeBCYSNAAPhJQCfdWsQRJajhE2PCsMkcBCNrElD fl8Anj1vbyMMby0oAWkmFllyDmVsqlnS =cRDg -----END PGP SIGNATURE-----
# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: [EMAIL PROTECTED] # target_branch: http://bzr.arbash-meinel.com/plugins/DEACTIVATED/gtk-\ # trunk # testament_sha1: bbc29adc77ebafd36b516412449c1e2b8996c871 # timestamp: 2008-11-13 01:05:21 -0600 # source_branch: http://bzr.arbash-meinel.com/plugins/gtk # base_revision_id: [EMAIL PROTECTED] # # Begin patch === modified file 'commit.py' --- commit.py 2008-09-29 16:10:51 +0000 +++ commit.py 2008-11-13 05:48:35 +0000 @@ -97,6 +97,13 @@ return pm +_newline_variants_re = re.compile(r'\r\n?') +def _sanitize_and_decode_message(utf8_message): + """Turn a utf-8 message into a sanitized Unicode message.""" + fixed_newline = _newline_variants_re.sub('\n', utf8_message) + return fixed_newline.decode('utf-8') + + class CommitDialog(gtk.Dialog): """Implementation of Commit.""" @@ -631,10 +638,12 @@ if self._commit_all_changes or record[2]:# [2] checkbox file_id = record[0] # [0] file_id path = record[1] # [1] real path - file_message = record[5] # [5] commit message + # [5] commit message + file_message = _sanitize_and_decode_message(record[5]) files.append(path.decode('UTF-8')) if self._enable_per_file_commits and file_message: # All of this needs to be utf-8 information + file_message = file_message.encode('UTF-8') file_info.append({'path':path, 'file_id':file_id, 'message':file_message}) file_info.sort(key=lambda x:(x['path'], x['file_id'])) @@ -712,7 +721,8 @@ def _get_global_commit_message(self): buf = self._global_message_text_view.get_buffer() start, end = buf.get_bounds() - return buf.get_text(start, end).decode('utf-8') + text = buf.get_text(start, end) + return _sanitize_and_decode_message(text) def _set_global_commit_message(self, message): """Just a helper for the test suite.""" === modified file 'revisionview.py' --- revisionview.py 2008-07-11 15:46:32 +0000 +++ revisionview.py 2008-11-13 06:55:41 +0000 @@ -22,6 +22,7 @@ import gobject import webbrowser +from bzrlib import trace from bzrlib.osutils import format_date from bzrlib.util.bencode import bdecode from bzrlib.testament import Testament @@ -422,10 +423,15 @@ self._add_parents_or_children(revision.parent_ids, self.parents_widgets, self.parents_table) - + file_info = revision.properties.get('file-info', None) if file_info is not None: - file_info = bdecode(file_info.encode('UTF-8')) + try: + file_info = bdecode(file_info.encode('UTF-8')) + except ValueError: + trace.note('Invalid per-file info for revision:%s, value: %r', + revision.revision_id, file_info) + file_info = None if file_info: if self._file_id is None: === modified file 'tests/__init__.py' --- tests/__init__.py 2008-06-29 22:44:42 +0000 +++ tests/__init__.py 2008-11-13 06:55:41 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2007 Jelmer Vernooij <[EMAIL PROTECTED]> +# Copyright (C) 2007, 2008 Jelmer Vernooij <[EMAIL PROTECTED]> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,9 +27,10 @@ testmod_names = [ 'test_commit', 'test_diff', + 'test_history', 'test_linegraph', 'test_preferences', - 'test_history', + 'test_revisionview', ] result.addTest(loader.loadTestsFromModuleNames(["%s.%s" % (__name__, i) for i in testmod_names])) === modified file 'tests/test_commit.py' --- tests/test_commit.py 2008-09-29 16:10:51 +0000 +++ tests/test_commit.py 2008-11-13 06:55:41 +0000 @@ -1,4 +1,4 @@ -# Copyright (C) 2007 John Arbash Meinel <[EMAIL PROTECTED]> +# Copyright (C) 2007, 2008 John Arbash Meinel <[EMAIL PROTECTED]> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -644,6 +644,28 @@ 'message':'message\nfor b_dir\n'}, ]), dlg._get_specific_files()) + def test_specific_files_sanitizes_messages(self): + tree = self.make_branch_and_tree('tree') + tree.branch.get_config().set_user_option('per_file_commits', 'true') + self.build_tree(['tree/a_file', 'tree/b_dir/']) + tree.add(['a_file', 'b_dir'], ['1a-id', '0b-id']) + + dlg = commit.CommitDialog(tree) + dlg._commit_selected_radio.set_active(True) + self.assertEqual((['a_file', 'b_dir'], []), dlg._get_specific_files()) + + dlg._treeview_files.set_cursor((1,)) + dlg._set_file_commit_message('Test\r\nmessage\rfor a_file\n') + dlg._treeview_files.set_cursor((2,)) + dlg._set_file_commit_message('message\r\nfor\nb_dir\r') + + self.assertEqual((['a_file', 'b_dir'], + [{'path':'a_file', 'file_id':'1a-id', + 'message':'Test\nmessage\nfor a_file\n'}, + {'path':'b_dir', 'file_id':'0b-id', + 'message':'message\nfor\nb_dir\n'}, + ]), dlg._get_specific_files()) + class TestCommitDialog_Commit(tests.TestCaseWithTransport): """Tests on the actual 'commit' button being pushed.""" @@ -687,6 +709,21 @@ self.assertEqual(last_rev, dlg.committed_revision_id) self.assertEqual(rev_id1, tree.branch.last_revision()) + def test_commit_global_sanitizes_message(self): + tree = self.make_branch_and_tree('tree') + self.build_tree(['tree/a']) + tree.add(['a'], ['a-id']) + rev_id1 = tree.commit('one') + + self.build_tree(['tree/b']) + tree.add(['b'], ['b-id']) + dlg = commit.CommitDialog(tree) + # With the check box set, it should only effect the local branch + dlg._set_global_commit_message('Commit\r\nmessage\rfoo\n') + dlg._do_commit() + rev = tree.branch.repository.get_revision(tree.last_revision()) + self.assertEqual('Commit\nmessage\nfoo\n', rev.message) + def test_bound_commit_both(self): tree = self.make_branch_and_tree('tree') self.build_tree(['tree/a']) @@ -1030,3 +1067,22 @@ {'path':u'\u03a9', 'file_id':'omega-id', 'message':u'\u03a9 is the end of all things.\n'}, ], file_info_decoded) + + +class TestSanitizeMessage(tests.TestCase): + + def assertSanitize(self, expected, original): + self.assertEqual(expected, + commit._sanitize_and_decode_message(original)) + + def test_untouched(self): + self.assertSanitize('foo\nbar\nbaz\n', 'foo\nbar\nbaz\n') + + def test_converts_cr_to_lf(self): + self.assertSanitize('foo\nbar\nbaz\n', 'foo\rbar\rbaz\r') + + def test_converts_crlf_to_lf(self): + self.assertSanitize('foo\nbar\nbaz\n', 'foo\r\nbar\r\nbaz\r\n') + + def test_converts_mixed_to_lf(self): + self.assertSanitize('foo\nbar\nbaz\n', 'foo\r\nbar\rbaz\n') === added file 'tests/test_revisionview.py' --- tests/test_revisionview.py 1970-01-01 00:00:00 +0000 +++ tests/test_revisionview.py 2008-11-13 06:55:41 +0000 @@ -0,0 +1,92 @@ +# Copyright (C) 2007, 2008 John Arbash Meinel <[EMAIL PROTECTED]> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""Test the RevisionView functionality.""" + +import os + +import gtk + +from bzrlib import ( + tests, + revision, + ) +from bzrlib.util import bencode + +from bzrlib.plugins.gtk import revisionview + + +class TestPendingRevisions(tests.TestCaseWithMemoryTransport): + + def assertBufferText(self, text, buffer): + """Check the text stored in the buffer.""" + self.assertEqual(text, buffer.get_text(buffer.get_start_iter(), + buffer.get_end_iter())) + + def test_create_view(self): + builder = self.make_branch_builder('test') + builder.build_snapshot('A', None, + [('add', ('', 'root-id', 'directory', None))]) + b = builder.get_branch() + + rv = revisionview.RevisionView(b) + rev = b.repository.get_revision('A') + rv.set_revision(rev) + self.assertEqual(rev.committer, rv.committer.get_text()) + self.assertFalse(rv.author.get_property('visible')) + self.assertFalse(rv.author_label.get_property('visible')) + self.assertFalse(rv.file_info_box.get_property('visible')) + + def test_create_view_with_file_info(self): + tree = self.make_branch_and_memory_tree('test') + file_info = bencode.bencode([{'file_id':'root-id', 'path':'', + 'message':'test-message\n'}]) + tree.lock_write() + try: + tree.add([''], ['root-id']) + tree.commit('test', rev_id='A', revprops={'file-info': file_info}) + finally: + tree.unlock() + b = tree.branch + + rv = revisionview.RevisionView(b) + rev = b.repository.get_revision('A') + rv.set_revision(rev) + + self.assertEqual(rev.committer, rv.committer.get_text()) + self.assertTrue(rv.file_info_box.get_property('visible')) + self.assertBufferText('\ntest-message\n', rv.file_info_buffer) + + def test_create_view_with_broken_file_info(self): + tree = self.make_branch_and_memory_tree('test') + # This should be 'message13:' + file_info = 'ld7:file_id7:root-id7:message11:test-message\n4:path0:ee' + tree.lock_write() + try: + tree.add([''], ['root-id']) + tree.commit('test', rev_id='A', revprops={'file-info': file_info}) + finally: + tree.unlock() + b = tree.branch + + rv = revisionview.RevisionView(b) + rev = b.repository.get_revision('A') + rv.set_revision(rev) + + self.assertEqual(rev.committer, rv.committer.get_text()) + self.assertFalse(rv.file_info_box.get_property('visible')) + log = self._get_log(True) + self.assertContainsRe(log, 'Invalid per-file info for revision:A') # Begin bundle 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 orB6j2n0OWcwL/i7kinChIVJfMQA
-- bzr-gtk mailing list [email protected] Modify settings or unsubscribe at: https://lists.canonical.com/mailman/listinfo/bzr-gtk
