/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to commit.py

  • Committer: Vincent Ladeuil
  • Date: 2009-05-28 15:14:14 UTC
  • mto: This revision was merged to the branch mainline in revision 640.
  • Revision ID: v.ladeuil+lp@free.fr-20090528151414-q5rlh8kaicx2hgqo
Implement commit message saving without modifying bzrlib.

* tests/test_commit.py:
(TestSavedCommitMessages.setUp): Install the post_uncommit hook
for all relevant tests.
(TestUncommitHook.setUp): Use explicit rev-ids to ease debugging.

* commit.py: 
Fix imports. Integrate SavedCommitMessagesManager so that we don't
need to modify bzrlib anymore.
(CommitDialog.__init__, CommitDialog._fill_in_files,
CommitDialog._construct_global_message, CommitDialog._do_cancel,
CommitDialog._do_commit): Stop testing can_save_commit_messages,
SavedCommitMessagesManager is always available now.
(SavedCommitMessagesManager): Borrowed from Anne Mohsen's patch.
(save_commit_messages): Implement the post_uncommit hook.

* __init__.py:
Install a lazy hook.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
import os.path
 
18
import re
 
19
 
17
20
try:
18
21
    import pygtk
19
22
    pygtk.require("2.0")
24
27
import gobject
25
28
import pango
26
29
 
27
 
import os.path
28
 
import re
29
 
 
30
 
from bzrlib import errors, osutils
31
 
from bzrlib.trace import mutter
 
30
from bzrlib import (
 
31
    branch,
 
32
    errors,
 
33
    osutils,
 
34
    trace,
 
35
    )
32
36
from bzrlib.util import bencode
33
37
 
34
38
from bzrlib.plugins.gtk import _i18n
35
 
from dialog import error_dialog, question_dialog
36
 
from errors import show_bzr_error
 
39
from bzrlib.plugins.gtk.dialog import question_dialog
 
40
from bzrlib.plugins.gtk.errors import show_bzr_error
37
41
 
38
42
try:
39
43
    import dbus
97
101
    return pm
98
102
 
99
103
 
 
104
_newline_variants_re = re.compile(r'\r\n?')
 
105
def _sanitize_and_decode_message(utf8_message):
 
106
    """Turn a utf-8 message into a sanitized Unicode message."""
 
107
    fixed_newline = _newline_variants_re.sub('\n', utf8_message)
 
108
    return fixed_newline.decode('utf-8')
 
109
 
 
110
 
100
111
class CommitDialog(gtk.Dialog):
101
112
    """Implementation of Commit."""
102
113
 
103
114
    def __init__(self, wt, selected=None, parent=None):
104
 
        gtk.Dialog.__init__(self, title="Commit - Olive",
105
 
                                  parent=parent,
106
 
                                  flags=0,
107
 
                                  buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
 
115
        gtk.Dialog.__init__(self, title="Commit to %s" % wt.basedir,
 
116
                            parent=parent, flags=0,)
 
117
        self.connect('delete-event', self._on_delete_window)
108
118
        self._question_dialog = question_dialog
109
119
 
 
120
        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
 
121
 
110
122
        self._wt = wt
111
123
        # TODO: Do something with this value, it is used by Olive
112
124
        #       It used to set all changes but this one to False
114
126
        self._enable_per_file_commits = True
115
127
        self._commit_all_changes = True
116
128
        self.committed_revision_id = None # Nothing has been committed yet
 
129
        self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
117
130
 
118
131
        self.setup_params()
119
132
        self.construct()
188
201
        self._basis_tree.lock_read()
189
202
        try:
190
203
            from diff import iter_changes_to_status
 
204
            saved_file_messages = self._saved_commit_messages_manager.get()[1]
191
205
            for (file_id, real_path, change_type, display_path
192
206
                ) in iter_changes_to_status(self._basis_tree, self._wt):
193
207
                if self._selected and real_path != self._selected:
194
208
                    enabled = False
195
209
                else:
196
210
                    enabled = True
 
211
                try:
 
212
                    default_message = saved_file_messages[file_id]
 
213
                except KeyError:
 
214
                    default_message = ''
197
215
                item_iter = store.append([
198
216
                    file_id,
199
217
                    real_path.encode('UTF-8'),
200
218
                    enabled,
201
219
                    display_path.encode('UTF-8'),
202
220
                    change_type,
203
 
                    '', # Initial comment
 
221
                    default_message, # Initial comment
204
222
                    ])
205
223
                if self._selected and enabled:
206
224
                    initial_cursor = store.get_path(item_iter)
232
250
                proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
233
251
                                           '/org/freedesktop/NetworkManager')
234
252
            except dbus.DBusException:
235
 
                mutter("networkmanager not available.")
 
253
                trace.mutter("networkmanager not available.")
236
254
                self._check_local.show()
237
255
                return
238
256
            
244
262
            except dbus.DBusException, e:
245
263
                # Silently drop errors. While DBus may be
246
264
                # available, NetworkManager doesn't necessarily have to be
247
 
                mutter("unable to get networkmanager state: %r" % e)
 
265
                trace.mutter("unable to get networkmanager state: %r" % e)
248
266
        self._check_local.show()
249
267
 
250
268
    def _fill_in_per_file_info(self):
338
356
        self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
339
357
 
340
358
    def _construct_action_pane(self):
 
359
        self._button_cancel = gtk.Button(stock=gtk.STOCK_CANCEL)
 
360
        self._button_cancel.connect('clicked', self._on_cancel_clicked)
 
361
        self._button_cancel.show()
 
362
        self.action_area.pack_end(self._button_cancel)
341
363
        self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
342
364
        self._button_commit.connect('clicked', self._on_commit_clicked)
343
365
        self._button_commit.set_flags(gtk.CAN_DEFAULT)
539
561
        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
540
562
 
541
563
        self._global_message_text_view = gtk.TextView()
 
564
        self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
542
565
        self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
543
566
        scroller.add(self._global_message_text_view)
544
567
        scroller.set_shadow_type(gtk.SHADOW_IN)
630
653
            if self._commit_all_changes or record[2]:# [2] checkbox
631
654
                file_id = record[0] # [0] file_id
632
655
                path = record[1]    # [1] real path
633
 
                file_message = record[5] # [5] commit message
 
656
                # [5] commit message
 
657
                file_message = _sanitize_and_decode_message(record[5])
634
658
                files.append(path.decode('UTF-8'))
635
659
                if self._enable_per_file_commits and file_message:
636
660
                    # All of this needs to be utf-8 information
 
661
                    file_message = file_message.encode('UTF-8')
637
662
                    file_info.append({'path':path, 'file_id':file_id,
638
663
                                     'message':file_message})
639
664
        file_info.sort(key=lambda x:(x['path'], x['file_id']))
643
668
            return files, []
644
669
 
645
670
    @show_bzr_error
 
671
    def _on_cancel_clicked(self, button):
 
672
        """ Cancel button clicked handler. """
 
673
        self._do_cancel()
 
674
 
 
675
    @show_bzr_error
 
676
    def _on_delete_window(self, source, event):
 
677
        """ Delete window handler. """
 
678
        self._do_cancel()
 
679
 
 
680
    def _do_cancel(self):
 
681
        """If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
 
682
        mgr = SavedCommitMessagesManager()
 
683
        self._saved_commit_messages_manager = mgr
 
684
        mgr.insert(self._get_global_commit_message(),
 
685
                   self._get_specific_files()[1])
 
686
        if mgr.is_not_empty(): # maybe worth saving
 
687
            response = self._question_dialog(
 
688
                _i18n('Commit cancelled'),
 
689
                _i18n('Do you want to save your commit messages ?'),
 
690
                parent=self)
 
691
            if response == gtk.RESPONSE_NO:
 
692
                 # save nothing and destroy old comments if any
 
693
                mgr = SavedCommitMessagesManager()
 
694
        mgr.save(self._wt, self._wt.branch)
 
695
        self.response(gtk.RESPONSE_CANCEL) # close window
 
696
 
 
697
    @show_bzr_error
646
698
    def _on_commit_clicked(self, button):
647
699
        """ Commit button clicked handler. """
648
700
        self._do_commit()
653
705
        if message == '':
654
706
            response = self._question_dialog(
655
707
                _i18n('Commit with an empty message?'),
656
 
                _i18n('You can describe your commit intent in the message.'))
 
708
                _i18n('You can describe your commit intent in the message.'),
 
709
                parent=self)
657
710
            if response == gtk.RESPONSE_NO:
658
711
                # Kindly give focus to message area
659
712
                self._global_message_text_view.grab_focus()
673
726
        for path in self._wt.unknowns():
674
727
            response = self._question_dialog(
675
728
                _i18n("Commit with unknowns?"),
676
 
                _i18n("Unknown files exist in the working tree. Commit anyway?"))
 
729
                _i18n("Unknown files exist in the working tree. Commit anyway?"),
 
730
                parent=self)
 
731
                # Doesn't set a parent for the dialog..
677
732
            if response == gtk.RESPONSE_NO:
678
733
                return
679
734
            break
693
748
            response = self._question_dialog(
694
749
                _i18n('Commit with no changes?'),
695
750
                _i18n('There are no changes in the working tree.'
696
 
                      ' Do you want to commit anyway?'))
 
751
                      ' Do you want to commit anyway?'),
 
752
                parent=self)
697
753
            if response == gtk.RESPONSE_YES:
698
754
                rev_id = self._wt.commit(message,
699
755
                               allow_pointless=True,
702
758
                               specific_files=specific_files,
703
759
                               revprops=revprops)
704
760
        self.committed_revision_id = rev_id
 
761
        # destroy old comments if any
 
762
        SavedCommitMessagesManager().save(self._wt, self._wt.branch)
705
763
        self.response(gtk.RESPONSE_OK)
706
764
 
707
765
    def _get_global_commit_message(self):
708
766
        buf = self._global_message_text_view.get_buffer()
709
767
        start, end = buf.get_bounds()
710
 
        return buf.get_text(start, end).decode('utf-8')
 
768
        text = buf.get_text(start, end)
 
769
        return _sanitize_and_decode_message(text)
711
770
 
712
771
    def _set_global_commit_message(self, message):
713
772
        """Just a helper for the test suite."""
734
793
                                       show_offset=False)
735
794
        rev_dict['revision_id'] = rev.revision_id
736
795
        return rev_dict
 
796
 
 
797
 
 
798
class SavedCommitMessagesManager:
 
799
    """Saves global commit message and utf-8 file_id->message dictionary
 
800
    of per-file commit messages on disk. Re-reads them later for re-using."""
 
801
    def __init__(self, tree=None, branch=None):
 
802
        """If branch is None, builds empty messages, otherwise reads them
 
803
        from branch's disk storage. 'tree' argument is for the future."""
 
804
        if branch is None:
 
805
            self.global_message = u''
 
806
            self.file_messages = {}
 
807
        else:
 
808
            config = branch.get_config()._get_branch_data_config()
 
809
            self.global_message = config.get_user_option('gtk_global_commit_message')
 
810
            if self.global_message is None:
 
811
                self.global_message = u''
 
812
            file_messages = config.get_user_option('gtk_file_commit_messages')
 
813
            if file_messages: # unicode and B-encoded:
 
814
                self.file_messages = bencode.bdecode(file_messages.encode('UTF-8'))
 
815
            else:
 
816
                self.file_messages = {}
 
817
    def get(self):
 
818
        return self.global_message, self.file_messages
 
819
    def is_not_empty(self):
 
820
        return bool(self.global_message or self.file_messages)
 
821
    def insert(self, global_message, file_info):
 
822
        """Formats per-file commit messages (list of dictionaries, one per file)
 
823
        into one utf-8 file_id->message dictionary and merges this with
 
824
        previously existing dictionary. Merges global commit message too."""
 
825
        file_messages = {}
 
826
        for fi in file_info:
 
827
            file_message = fi['message']
 
828
            if file_message:
 
829
                file_messages[fi['file_id']] = file_message # utf-8 strings
 
830
        for k,v in file_messages.iteritems():
 
831
            try:
 
832
                self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
 
833
            except KeyError:
 
834
                self.file_messages[k] = v
 
835
        if self.global_message:
 
836
            self.global_message = global_message + '\n******\n' + self.global_message
 
837
        else:
 
838
            self.global_message = global_message
 
839
    def save(self, tree, branch):
 
840
        # We store in branch's config, which can be a problem if two gcommit
 
841
        # are done in two checkouts of one single branch (comments overwrite
 
842
        # each other). Ideally should be in working tree. But uncommit does
 
843
        # not always have a working tree, though it always has a branch.
 
844
        # 'tree' argument is for the future
 
845
        config = branch.get_config()
 
846
        # should it be named "gtk_" or some more neutral name ("gui_" ?) to
 
847
        # be compatible with qbzr in the future?
 
848
        config.set_user_option('gtk_global_commit_message', self.global_message)
 
849
        # bencode() does not know unicode objects but set_user_option() requires one:
 
850
        config.set_user_option('gtk_file_commit_messages',
 
851
                                bencode.bencode(self.file_messages).decode('UTF-8'))
 
852
 
 
853
 
 
854
def save_commit_messages(local, master, old_revno, old_revid,
 
855
                         new_revno, new_revid):
 
856
    b = local
 
857
    if b is None:
 
858
        b = master
 
859
    mgr = SavedCommitMessagesManager(None, b)
 
860
    revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
 
861
    cur_revno = old_revno
 
862
    new_revision_id = old_revid
 
863
    graph = b.repository.get_graph()
 
864
    for rev_id in revid_iterator:
 
865
        if cur_revno == new_revno:
 
866
            break
 
867
        cur_revno -= 1
 
868
        rev = b.repository.get_revision(rev_id)
 
869
        file_info = rev.properties.get('file-info', None)
 
870
        if file_info is None:
 
871
            file_info = {}
 
872
        else:
 
873
            file_info = bencode.bdecode(file_info.encode('UTF-8'))
 
874
        global_message = osutils.safe_unicode(rev.message)
 
875
        # Concatenate comment of the uncommitted revision
 
876
        mgr.insert(global_message, file_info)
 
877
 
 
878
        parents = graph.get_parent_map([rev_id]).get(rev_id, None)
 
879
        if not parents:
 
880
            continue
 
881
    mgr.save(None, b)