Based on last Anne Mohsen's patch, this fix:

- doesn't modify bzrlib at all but use a post_uncommit hook instead,
- comes with tests :)

This still doesn't address several points raised in the bug comments
that I'd like us to address in the future:

- share a common save/restore mechanism with qbzr (which currently
  implements --fixes and --authors (but doesn't save them at uncommit
  time, also a single message is saved in qbzr case)),
- provide better ways to get at uncommit messages (ideally one can
  visualize the branch starting at the tip before the commit and just
  drag and drop from there to build its commit message(s).

Final note: feedback and tests before landing and release will be warmly
welcomed, there are now automated tests, but I may have missed some use
cases...

  Vincent

# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: [email protected]
# target_branch: bzr+ssh://bazaar.launchpad.net/%7Ebzr-gtk/bzr-\
#   gtk/trunk/
# testament_sha1: 6c98a39b3e13f455b71fd0aff8a0ec6b7ddc68d2
# timestamp: 2009-05-28 17:42:10 +0200
# base_revision_id: [email protected]\
#   h8uwips9hr8ybx4e
# 
# Begin patch
=== modified file 'NEWS'
--- NEWS	2009-05-27 09:40:13 +0000
+++ NEWS	2009-05-28 15:28:13 +0000
@@ -19,6 +19,10 @@
 
   * Fix gannotate.conf handling. (Vincent Ladeuil, #373157)
 
+  * Save commit messages at uncommit time, restore them at
+    gcommit time. Also allow saving commit messages if the commit
+    is cancelled. (Anne Mohsen, Vincent Ladeuil, #215674)
+
   * Mark as compatible with bzr 1.13.
 
  FEATURES

=== modified file '__init__.py'
--- __init__.py	2009-05-21 20:26:23 +0000
+++ __init__.py	2009-05-28 15:14:14 +0000
@@ -37,6 +37,7 @@
 import bzrlib
 import bzrlib.api
 from bzrlib import (
+    branch,
     config,
     errors,
     )
@@ -138,6 +139,13 @@
     plugin_cmds.register_lazy("cmd_%s" % cmd, aliases,
                               "bzrlib.plugins.gtk.commands")
 
+def save_commit_messages(*args):
+    from bzrlib.plugins.gtk import commit
+    commit.save_commit_messages(*args)
+
+branch.Branch.hooks.install_named_hook('post_uncommit',
+                                       save_commit_messages,
+                                       "Saving commit messages for gcommit")
 
 import gettext
 gettext.install('olive-gtk')

=== modified file 'commit.py'
--- commit.py	2009-04-08 08:06:58 +0000
+++ commit.py	2009-05-28 15:28:13 +0000
@@ -27,8 +27,12 @@
 import gobject
 import pango
 
-from bzrlib import errors, osutils
-from bzrlib.trace import mutter
+from bzrlib import (
+    branch,
+    errors,
+    osutils,
+    trace,
+    )
 from bzrlib.util import bencode
 
 from bzrlib.plugins.gtk import _i18n
@@ -109,12 +113,10 @@
 
     def __init__(self, wt, selected=None, parent=None):
         gtk.Dialog.__init__(self, title="Commit to %s" % wt.basedir,
-                                  parent=parent,
-                                  flags=0,
-                                  buttons=(gtk.STOCK_CANCEL,
-                                           gtk.RESPONSE_CANCEL))
+                            parent=parent, flags=0,)
+        self.connect('delete-event', self._on_delete_window)
         self._question_dialog = question_dialog
-        
+
         self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
 
         self._wt = wt
@@ -124,6 +126,7 @@
         self._enable_per_file_commits = True
         self._commit_all_changes = True
         self.committed_revision_id = None # Nothing has been committed yet
+        self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
 
         self.setup_params()
         self.construct()
@@ -198,19 +201,24 @@
         self._basis_tree.lock_read()
         try:
             from diff import iter_changes_to_status
+            saved_file_messages = self._saved_commit_messages_manager.get()[1]
             for (file_id, real_path, change_type, display_path
                 ) in iter_changes_to_status(self._basis_tree, self._wt):
                 if self._selected and real_path != self._selected:
                     enabled = False
                 else:
                     enabled = True
+                try:
+                    default_message = saved_file_messages[file_id]
+                except KeyError:
+                    default_message = ''
                 item_iter = store.append([
                     file_id,
                     real_path.encode('UTF-8'),
                     enabled,
                     display_path.encode('UTF-8'),
                     change_type,
-                    '', # Initial comment
+                    default_message, # Initial comment
                     ])
                 if self._selected and enabled:
                     initial_cursor = store.get_path(item_iter)
@@ -242,7 +250,7 @@
                 proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
                                            '/org/freedesktop/NetworkManager')
             except dbus.DBusException:
-                mutter("networkmanager not available.")
+                trace.mutter("networkmanager not available.")
                 self._check_local.show()
                 return
             
@@ -254,7 +262,7 @@
             except dbus.DBusException, e:
                 # Silently drop errors. While DBus may be
                 # available, NetworkManager doesn't necessarily have to be
-                mutter("unable to get networkmanager state: %r" % e)
+                trace.mutter("unable to get networkmanager state: %r" % e)
         self._check_local.show()
 
     def _fill_in_per_file_info(self):
@@ -348,6 +356,10 @@
         self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
 
     def _construct_action_pane(self):
+        self._button_cancel = gtk.Button(stock=gtk.STOCK_CANCEL)
+        self._button_cancel.connect('clicked', self._on_cancel_clicked)
+        self._button_cancel.show()
+        self.action_area.pack_end(self._button_cancel)
         self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
         self._button_commit.connect('clicked', self._on_commit_clicked)
         self._button_commit.set_flags(gtk.CAN_DEFAULT)
@@ -549,6 +561,7 @@
         scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
         self._global_message_text_view = gtk.TextView()
+        self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
         self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
         scroller.add(self._global_message_text_view)
         scroller.set_shadow_type(gtk.SHADOW_IN)
@@ -655,6 +668,33 @@
             return files, []
 
     @show_bzr_error
+    def _on_cancel_clicked(self, button):
+        """ Cancel button clicked handler. """
+        self._do_cancel()
+
+    @show_bzr_error
+    def _on_delete_window(self, source, event):
+        """ Delete window handler. """
+        self._do_cancel()
+
+    def _do_cancel(self):
+        """If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
+        mgr = SavedCommitMessagesManager()
+        self._saved_commit_messages_manager = mgr
+        mgr.insert(self._get_global_commit_message(),
+                   self._get_specific_files()[1])
+        if mgr.is_not_empty(): # maybe worth saving
+            response = self._question_dialog(
+                _i18n('Commit cancelled'),
+                _i18n('Do you want to save your commit messages ?'),
+                parent=self)
+            if response == gtk.RESPONSE_NO:
+                 # save nothing and destroy old comments if any
+                mgr = SavedCommitMessagesManager()
+        mgr.save(self._wt, self._wt.branch)
+        self.response(gtk.RESPONSE_CANCEL) # close window
+
+    @show_bzr_error
     def _on_commit_clicked(self, button):
         """ Commit button clicked handler. """
         self._do_commit()
@@ -718,6 +758,8 @@
                                specific_files=specific_files,
                                revprops=revprops)
         self.committed_revision_id = rev_id
+        # destroy old comments if any
+        SavedCommitMessagesManager().save(self._wt, self._wt.branch)
         self.response(gtk.RESPONSE_OK)
 
     def _get_global_commit_message(self):
@@ -751,3 +793,102 @@
                                        show_offset=False)
         rev_dict['revision_id'] = rev.revision_id
         return rev_dict
+
+
+class SavedCommitMessagesManager:
+    """Save glogal and per-file commit messages.
+
+    Saves global commit message and utf-8 file_id->message dictionary
+    of per-file commit messages on disk. Re-reads them later for re-using.
+    """
+
+    def __init__(self, tree=None, branch=None):
+        """If branch is None, builds empty messages, otherwise reads them
+        from branch's disk storage. 'tree' argument is for the future."""
+        if branch is None:
+            self.global_message = u''
+            self.file_messages = {}
+        else:
+            config = branch.get_config()._get_branch_data_config()
+            self.global_message = config.get_user_option(
+                'gtk_global_commit_message')
+            if self.global_message is None:
+                self.global_message = u''
+            file_messages = config.get_user_option('gtk_file_commit_messages')
+            if file_messages: # unicode and B-encoded:
+                self.file_messages = bencode.bdecode(
+                    file_messages.encode('UTF-8'))
+            else:
+                self.file_messages = {}
+
+    def get(self):
+        return self.global_message, self.file_messages
+
+    def is_not_empty(self):
+        return bool(self.global_message or self.file_messages)
+
+    def insert(self, global_message, file_info):
+        """Formats per-file commit messages (list of dictionaries, one per file)
+        into one utf-8 file_id->message dictionary and merges this with
+        previously existing dictionary. Merges global commit message too."""
+        file_messages = {}
+        for fi in file_info:
+            file_message = fi['message']
+            if file_message:
+                file_messages[fi['file_id']] = file_message # utf-8 strings
+        for k,v in file_messages.iteritems():
+            try:
+                self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
+            except KeyError:
+                self.file_messages[k] = v
+        if self.global_message:
+            self.global_message = global_message + '\n******\n' \
+                + self.global_message
+        else:
+            self.global_message = global_message
+
+    def save(self, tree, branch):
+        # We store in branch's config, which can be a problem if two gcommit
+        # are done in two checkouts of one single branch (comments overwrite
+        # each other). Ideally should be in working tree. But uncommit does
+        # not always have a working tree, though it always has a branch.
+        # 'tree' argument is for the future
+        config = branch.get_config()
+        # should it be named "gtk_" or some more neutral name ("gui_" ?) to
+        # be compatible with qbzr in the future?
+        config.set_user_option('gtk_global_commit_message', self.global_message)
+        # bencode() does not know unicode objects but set_user_option()
+        # requires one:
+        config.set_user_option(
+            'gtk_file_commit_messages',
+            bencode.bencode(self.file_messages).decode('UTF-8'))
+
+
+def save_commit_messages(local, master, old_revno, old_revid,
+                         new_revno, new_revid):
+    b = local
+    if b is None:
+        b = master
+    mgr = SavedCommitMessagesManager(None, b)
+    revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
+    cur_revno = old_revno
+    new_revision_id = old_revid
+    graph = b.repository.get_graph()
+    for rev_id in revid_iterator:
+        if cur_revno == new_revno:
+            break
+        cur_revno -= 1
+        rev = b.repository.get_revision(rev_id)
+        file_info = rev.properties.get('file-info', None)
+        if file_info is None:
+            file_info = {}
+        else:
+            file_info = bencode.bdecode(file_info.encode('UTF-8'))
+        global_message = osutils.safe_unicode(rev.message)
+        # Concatenate comment of the uncommitted revision
+        mgr.insert(global_message, file_info)
+
+        parents = graph.get_parent_map([rev_id]).get(rev_id, None)
+        if not parents:
+            continue
+    mgr.save(None, b)

=== modified file 'tests/test_commit.py'
--- tests/test_commit.py	2008-11-13 06:55:41 +0000
+++ tests/test_commit.py	2009-05-28 15:14:14 +0000
@@ -21,8 +21,10 @@
 import gtk
 
 from bzrlib import (
+    branch,
+    revision,
     tests,
-    revision,
+    uncommit,
     )
 from bzrlib.util import bencode
 
@@ -667,8 +669,7 @@
                           ]), dlg._get_specific_files())
 
 
-class TestCommitDialog_Commit(tests.TestCaseWithTransport):
-    """Tests on the actual 'commit' button being pushed."""
+class QuestionHelpers(object):
 
     def _set_question_yes(self, dlg):
         """Set the dialog to answer YES to any questions."""
@@ -688,6 +689,10 @@
             return gtk.RESPONSE_NO
         dlg._question_dialog = _question_no
 
+
+class TestCommitDialog_Commit(tests.TestCaseWithTransport, QuestionHelpers):
+    """Tests on the actual 'commit' button being pushed."""
+
     def test_bound_commit_local(self):
         tree = self.make_branch_and_tree('tree')
         self.build_tree(['tree/a'])
@@ -1086,3 +1091,134 @@
 
     def test_converts_mixed_to_lf(self):
         self.assertSanitize('foo\nbar\nbaz\n', 'foo\r\nbar\rbaz\n')
+
+
+class TestSavedCommitMessages(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestSavedCommitMessages, self).setUp()
+        # Install our hook
+        branch.Branch.hooks.install_named_hook(
+            'post_uncommit', commit.save_commit_messages, None)
+
+    def _get_file_info_dict(self, rank):
+        file_info = [dict(path='a', file_id='a-id', message='a msg %d' % rank),
+                     dict(path='b', file_id='b-id', message='b msg %d' % rank)]
+        return file_info
+
+    def _get_file_info_revprops(self, rank):
+        file_info_prop = self._get_file_info_dict(rank)
+        return {'file-info': bencode.bencode(file_info_prop).decode('UTF-8')}
+
+    def _get_commit_message(self):
+        return self.config.get_user_option('gtk_global_commit_message')
+
+    def _get_file_commit_messages(self):
+        return self.config.get_user_option('gtk_file_commit_messages')
+
+
+class TestUncommitHook(TestSavedCommitMessages):
+
+    def setUp(self):
+        super(TestUncommitHook, self).setUp()
+        self.tree = self.make_branch_and_tree('tree')
+        self.config = self.tree.branch.get_config()
+        self.build_tree(['tree/a', 'tree/b'])
+        self.tree.add(['a'], ['a-id'])
+        self.tree.add(['b'], ['b-id'])
+        rev1 = self.tree.commit('one', rev_id='one-id',
+                                revprops=self._get_file_info_revprops(1))
+        rev2 = self.tree.commit('two', rev_id='two-id',
+                                revprops=self._get_file_info_revprops(2))
+        rev3 = self.tree.commit('three', rev_id='three-id',
+                                revprops=self._get_file_info_revprops(3))
+
+    def test_uncommit_one_by_one(self):
+        uncommit.uncommit(self.tree.branch, tree=self.tree)
+        self.assertEquals(u'three', self._get_commit_message())
+        self.assertEquals(u'd4:a-id7:a msg 34:b-id7:b msg 3e',
+                          self._get_file_commit_messages())
+
+        uncommit.uncommit(self.tree.branch, tree=self.tree)
+        self.assertEquals(u'two\n******\nthree', self._get_commit_message())
+        self.assertEquals(u'd4:a-id22:a msg 2\n******\na msg 3'
+                          '4:b-id22:b msg 2\n******\nb msg 3e',
+                          self._get_file_commit_messages())
+
+        uncommit.uncommit(self.tree.branch, tree=self.tree)
+        self.assertEquals(u'one\n******\ntwo\n******\nthree',
+                          self._get_commit_message())
+        self.assertEquals(u'd4:a-id37:a msg 1\n******\na msg 2\n******\na msg 3'
+                          '4:b-id37:b msg 1\n******\nb msg 2\n******\nb msg 3e',
+                          self._get_file_commit_messages())
+
+    def test_uncommit_all_at_once(self):
+        uncommit.uncommit(self.tree.branch, tree=self.tree, revno=1)
+        self.assertEquals(u'one\n******\ntwo\n******\nthree',
+                          self._get_commit_message())
+        self.assertEquals(u'd4:a-id37:a msg 1\n******\na msg 2\n******\na msg 3'
+                          '4:b-id37:b msg 1\n******\nb msg 2\n******\nb msg 3e',
+                          self._get_file_commit_messages())
+
+
+class TestReusingSavedCommitMessages(TestSavedCommitMessages, QuestionHelpers):
+
+    def setUp(self):
+        super(TestReusingSavedCommitMessages, self).setUp()
+        self.tree = self.make_branch_and_tree('tree')
+        self.config = self.tree.branch.get_config()
+        self.config.set_user_option('per_file_commits', 'true')
+        self.build_tree(['tree/a', 'tree/b'])
+        self.tree.add(['a'], ['a-id'])
+        self.tree.add(['b'], ['b-id'])
+        rev1 = self.tree.commit('one', revprops=self._get_file_info_revprops(1))
+        rev2 = self.tree.commit('two', revprops=self._get_file_info_revprops(2))
+        uncommit.uncommit(self.tree.branch, tree=self.tree)
+        self.build_tree_contents([('tree/a', 'new a content\n'),
+                                  ('tree/b', 'new b content'),])
+
+    def _get_commit_dialog(self, tree):
+        # Ensure we will never use a dialog that can actually prompt the user
+        # during the test suite. Test *can* and *should* override with the
+        # correct question dialog type.
+        dlg = commit.CommitDialog(tree)
+        self._set_question_no(dlg)
+        return dlg
+
+    def test_setup_saved_messages(self):
+        # Check the initial setup
+        self.assertEquals(u'two', self._get_commit_message())
+        self.assertEquals(u'd4:a-id7:a msg 24:b-id7:b msg 2e',
+                          self._get_file_commit_messages())
+
+    def test_messages_are_reloaded(self):
+        dlg = self._get_commit_dialog(self.tree)
+        self.assertEquals(u'two', dlg._get_global_commit_message())
+        self.assertEquals(([u'a', u'b'],
+                           [{ 'path': 'a',
+                             'file_id': 'a-id', 'message': 'a msg 2',},
+                           {'path': 'b',
+                            'file_id': 'b-id', 'message': 'b msg 2',}],),
+                          dlg._get_specific_files())
+
+    def test_messages_are_consumed(self):
+        dlg = self._get_commit_dialog(self.tree)
+        dlg._do_commit()
+        self.assertEquals(u'', self._get_commit_message())
+        self.assertEquals(u'de', self._get_file_commit_messages())
+
+    def test_messages_are_saved_on_cancel_if_required(self):
+        dlg = self._get_commit_dialog(self.tree)
+        self._set_question_yes(dlg) # Save messages
+        dlg._do_cancel()
+        self.assertEquals(u'two', self._get_commit_message())
+        self.assertEquals(u'd4:a-id7:a msg 24:b-id7:b msg 2e',
+                          self._get_file_commit_messages())
+
+    def test_messages_are_cleared_on_cancel_if_required(self):
+        dlg = self._get_commit_dialog(self.tree)
+        self._set_question_no(dlg) # Don't save messages
+        dlg._do_cancel()
+        self.assertEquals(u'', self._get_commit_message())
+        self.assertEquals(u'de',
+                          self._get_file_commit_messages())

# Begin bundle
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
9SxLsxB5+xwUt5Ebv8IR1R/xdyRThQkL8MaxcA==
-- 
bzr-gtk mailing list
[email protected]
Modify settings or unsubscribe at: 
https://lists.canonical.com/mailman/listinfo/bzr-gtk

Reply via email to