/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: Jasper Groenewegen
  • Date: 2009-12-22 22:35:52 UTC
  • mto: This revision was merged to the branch mainline in revision 678.
  • Revision ID: colbrac@xs4all.nl-20091222223552-c172tafb1bzm0mvk
Fix typo in checkout dialog (branck->branch)

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
32
 
from bzrlib.util import bencode
33
 
 
34
 
from dialog import error_dialog, question_dialog
35
 
from errors import show_bzr_error
 
30
from bzrlib import (
 
31
    branch,
 
32
    errors,
 
33
    osutils,
 
34
    trace,
 
35
    )
 
36
try:
 
37
    from bzrlib import bencode
 
38
except ImportError:
 
39
    from bzrlib.util import bencode
 
40
 
 
41
from bzrlib.plugins.gtk import _i18n
 
42
from bzrlib.plugins.gtk.dialog import question_dialog
 
43
from bzrlib.plugins.gtk.errors import show_bzr_error
36
44
 
37
45
try:
38
46
    import dbus
96
104
    return pm
97
105
 
98
106
 
 
107
_newline_variants_re = re.compile(r'\r\n?')
 
108
def _sanitize_and_decode_message(utf8_message):
 
109
    """Turn a utf-8 message into a sanitized Unicode message."""
 
110
    fixed_newline = _newline_variants_re.sub('\n', utf8_message)
 
111
    return fixed_newline.decode('utf-8')
 
112
 
 
113
 
99
114
class CommitDialog(gtk.Dialog):
100
115
    """Implementation of Commit."""
101
116
 
102
117
    def __init__(self, wt, selected=None, parent=None):
103
 
        gtk.Dialog.__init__(self, title="Commit - Olive",
104
 
                                  parent=parent,
105
 
                                  flags=0,
106
 
                                  buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
 
118
        gtk.Dialog.__init__(self, title="Commit to %s" % wt.basedir,
 
119
                            parent=parent, flags=0,)
 
120
        self.connect('delete-event', self._on_delete_window)
107
121
        self._question_dialog = question_dialog
108
122
 
 
123
        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
 
124
 
109
125
        self._wt = wt
110
126
        # TODO: Do something with this value, it is used by Olive
111
127
        #       It used to set all changes but this one to False
113
129
        self._enable_per_file_commits = True
114
130
        self._commit_all_changes = True
115
131
        self.committed_revision_id = None # Nothing has been committed yet
 
132
        self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
116
133
 
117
134
        self.setup_params()
118
135
        self.construct()
164
181
        store = self._files_store
165
182
        self._treeview_files.set_model(None)
166
183
 
167
 
        added = _('added')
168
 
        removed = _('removed')
169
 
        renamed = _('renamed')
170
 
        renamed_and_modified = _('renamed and modified')
171
 
        modified = _('modified')
172
 
        kind_changed = _('kind changed')
 
184
        added = _i18n('added')
 
185
        removed = _i18n('removed')
 
186
        renamed = _i18n('renamed')
 
187
        renamed_and_modified = _i18n('renamed and modified')
 
188
        modified = _i18n('modified')
 
189
        kind_changed = _i18n('kind changed')
173
190
 
174
191
        # The store holds:
175
192
        # [file_id, real path, checkbox, display path, changes type, message]
187
204
        self._basis_tree.lock_read()
188
205
        try:
189
206
            from diff import iter_changes_to_status
 
207
            saved_file_messages = self._saved_commit_messages_manager.get()[1]
190
208
            for (file_id, real_path, change_type, display_path
191
209
                ) in iter_changes_to_status(self._basis_tree, self._wt):
192
210
                if self._selected and real_path != self._selected:
193
211
                    enabled = False
194
212
                else:
195
213
                    enabled = True
 
214
                try:
 
215
                    default_message = saved_file_messages[file_id]
 
216
                except KeyError:
 
217
                    default_message = ''
196
218
                item_iter = store.append([
197
219
                    file_id,
198
220
                    real_path.encode('UTF-8'),
199
221
                    enabled,
200
222
                    display_path.encode('UTF-8'),
201
223
                    change_type,
202
 
                    '', # Initial comment
 
224
                    default_message, # Initial comment
203
225
                    ])
204
226
                if self._selected and enabled:
205
227
                    initial_cursor = store.get_path(item_iter)
231
253
                proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
232
254
                                           '/org/freedesktop/NetworkManager')
233
255
            except dbus.DBusException:
234
 
                mutter("networkmanager not available.")
 
256
                trace.mutter("networkmanager not available.")
235
257
                self._check_local.show()
236
258
                return
237
259
            
243
265
            except dbus.DBusException, e:
244
266
                # Silently drop errors. While DBus may be
245
267
                # available, NetworkManager doesn't necessarily have to be
246
 
                mutter("unable to get networkmanager state: %r" % e)
 
268
                trace.mutter("unable to get networkmanager state: %r" % e)
247
269
        self._check_local.show()
248
270
 
249
271
    def _fill_in_per_file_info(self):
257
279
            self._enable_per_file_commits = True
258
280
        if not self._enable_per_file_commits:
259
281
            self._file_message_expander.hide()
260
 
            self._global_message_label.set_markup(_('<b>Commit Message</b>'))
 
282
            self._global_message_label.set_markup(_i18n('<b>Commit Message</b>'))
261
283
 
262
284
    def _compute_delta(self):
263
285
        self._delta = self._wt.changes_from(self._basis_tree)
302
324
                            gtk.gdk.CONTROL_MASK, 0, self._on_accel_next)
303
325
        self.add_accel_group(group)
304
326
 
 
327
        # ignore the escape key (avoid closing the window)
 
328
        self.connect_object('close', self.emit_stop_by_name, 'close')
 
329
 
305
330
    def _construct_left_pane(self):
306
331
        self._left_pane_box = gtk.VBox(homogeneous=False, spacing=5)
307
332
        self._construct_file_list()
308
333
        self._construct_pending_list()
309
334
 
310
 
        self._check_local = gtk.CheckButton(_("_Only commit locally"),
 
335
        self._check_local = gtk.CheckButton(_i18n("_Only commit locally"),
311
336
                                            use_underline=True)
312
337
        self._left_pane_box.pack_end(self._check_local, False, False)
313
338
        self._check_local.set_active(False)
334
359
        self._hpane.pack2(self._right_pane_table, resize=True, shrink=True)
335
360
 
336
361
    def _construct_action_pane(self):
337
 
        self._button_commit = gtk.Button(_("Comm_it"), use_underline=True)
 
362
        self._button_cancel = gtk.Button(stock=gtk.STOCK_CANCEL)
 
363
        self._button_cancel.connect('clicked', self._on_cancel_clicked)
 
364
        self._button_cancel.show()
 
365
        self.action_area.pack_end(self._button_cancel)
 
366
        self._button_commit = gtk.Button(_i18n("Comm_it"), use_underline=True)
338
367
        self._button_commit.connect('clicked', self._on_commit_clicked)
339
368
        self._button_commit.set_flags(gtk.CAN_DEFAULT)
340
369
        self._button_commit.show()
360
389
 
361
390
    def _construct_file_list(self):
362
391
        self._files_box = gtk.VBox(homogeneous=False, spacing=0)
363
 
        file_label = gtk.Label(_('Files'))
 
392
        file_label = gtk.Label(_i18n('Files'))
364
393
        # file_label.show()
365
394
        self._files_box.pack_start(file_label, expand=False)
366
395
 
367
396
        self._commit_all_files_radio = gtk.RadioButton(
368
 
            None, _("Commit all changes"))
 
397
            None, _i18n("Commit all changes"))
369
398
        self._files_box.pack_start(self._commit_all_files_radio, expand=False)
370
399
        self._commit_all_files_radio.show()
371
400
        self._commit_all_files_radio.connect('toggled',
372
401
            self._toggle_commit_selection)
373
402
        self._commit_selected_radio = gtk.RadioButton(
374
 
            self._commit_all_files_radio, _("Only commit selected changes"))
 
403
            self._commit_all_files_radio, _i18n("Only commit selected changes"))
375
404
        self._files_box.pack_start(self._commit_selected_radio, expand=False)
376
405
        self._commit_selected_radio.show()
377
406
        self._commit_selected_radio.connect('toggled',
378
407
            self._toggle_commit_selection)
379
408
        if self._pending:
380
 
            self._commit_all_files_radio.set_label(_('Commit all changes*'))
 
409
            self._commit_all_files_radio.set_label(_i18n('Commit all changes*'))
381
410
            self._commit_all_files_radio.set_sensitive(False)
382
411
            self._commit_selected_radio.set_sensitive(False)
383
412
 
410
439
        crt.set_property('activatable', not bool(self._pending))
411
440
        crt.connect("toggled", self._toggle_commit, self._files_store)
412
441
        if self._pending:
413
 
            name = _('Commit*')
 
442
            name = _i18n('Commit*')
414
443
        else:
415
 
            name = _('Commit')
 
444
            name = _i18n('Commit')
416
445
        commit_col = gtk.TreeViewColumn(name, crt, active=2)
417
446
        commit_col.set_visible(False)
418
447
        self._treeview_files.append_column(commit_col)
419
 
        self._treeview_files.append_column(gtk.TreeViewColumn(_('Path'),
 
448
        self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Path'),
420
449
                                           gtk.CellRendererText(), text=3))
421
 
        self._treeview_files.append_column(gtk.TreeViewColumn(_('Type'),
 
450
        self._treeview_files.append_column(gtk.TreeViewColumn(_i18n('Type'),
422
451
                                           gtk.CellRendererText(), text=4))
423
452
        self._treeview_files.connect('cursor-changed',
424
453
                                     self._on_treeview_files_cursor_changed)
451
480
 
452
481
        pending_message = gtk.Label()
453
482
        pending_message.set_markup(
454
 
            _('<i>* Cannot select specific files when merging</i>'))
 
483
            _i18n('<i>* Cannot select specific files when merging</i>'))
455
484
        self._pending_box.pack_start(pending_message, expand=False, padding=5)
456
485
        pending_message.show()
457
486
 
458
 
        pending_label = gtk.Label(_('Pending Revisions'))
 
487
        pending_label = gtk.Label(_i18n('Pending Revisions'))
459
488
        self._pending_box.pack_start(pending_label, expand=False, padding=0)
460
489
        pending_label.show()
461
490
 
477
506
                                 )
478
507
        self._pending_store = liststore
479
508
        self._treeview_pending.set_model(liststore)
480
 
        self._treeview_pending.append_column(gtk.TreeViewColumn(_('Date'),
 
509
        self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Date'),
481
510
                                             gtk.CellRendererText(), text=1))
482
 
        self._treeview_pending.append_column(gtk.TreeViewColumn(_('Committer'),
 
511
        self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Committer'),
483
512
                                             gtk.CellRendererText(), text=2))
484
 
        self._treeview_pending.append_column(gtk.TreeViewColumn(_('Summary'),
 
513
        self._treeview_pending.append_column(gtk.TreeViewColumn(_i18n('Summary'),
485
514
                                             gtk.CellRendererText(), text=3))
486
515
 
487
516
    def _construct_diff_view(self):
491
520
        #       decide that we really don't ever want to display it, we should
492
521
        #       actually remove it, and other references to it, along with the
493
522
        #       tests that it is set properly.
494
 
        self._diff_label = gtk.Label(_('Diff for whole tree'))
 
523
        self._diff_label = gtk.Label(_i18n('Diff for whole tree'))
495
524
        self._diff_label.set_alignment(0, 0)
496
525
        self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
497
526
        self._add_to_right_table(self._diff_label, 1, False)
515
544
        self._file_message_text_view.set_accepts_tab(False)
516
545
        self._file_message_text_view.show()
517
546
 
518
 
        self._file_message_expander = gtk.Expander(_('File commit message'))
 
547
        self._file_message_expander = gtk.Expander(_i18n('File commit message'))
519
548
        self._file_message_expander.set_expanded(True)
520
549
        self._file_message_expander.add(scroller)
521
550
        self._add_to_right_table(self._file_message_expander, 1, False)
522
551
        self._file_message_expander.show()
523
552
 
524
553
    def _construct_global_message(self):
525
 
        self._global_message_label = gtk.Label(_('Global Commit Message'))
526
 
        self._global_message_label.set_markup(_('<b>Global Commit Message</b>'))
 
554
        self._global_message_label = gtk.Label(_i18n('Global Commit Message'))
 
555
        self._global_message_label.set_markup(
 
556
            _i18n('<b>Global Commit Message</b>'))
527
557
        self._global_message_label.set_alignment(0, 0)
528
558
        self._right_pane_table.set_row_spacing(self._right_pane_table_row, 0)
529
559
        self._add_to_right_table(self._global_message_label, 1, False)
534
564
        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
535
565
 
536
566
        self._global_message_text_view = gtk.TextView()
 
567
        self._set_global_commit_message(self._saved_commit_messages_manager.get()[0])
537
568
        self._global_message_text_view.modify_font(pango.FontDescription("Monospace"))
538
569
        scroller.add(self._global_message_text_view)
539
570
        scroller.set_shadow_type(gtk.SHADOW_IN)
549
580
 
550
581
        if selection is not None:
551
582
            path, display_path = model.get(selection, 1, 3)
552
 
            self._diff_label.set_text(_('Diff for ') + display_path)
 
583
            self._diff_label.set_text(_i18n('Diff for ') + display_path)
553
584
            if path is None:
554
585
                self._diff_view.show_diff(None)
555
586
            else:
596
627
        text_buffer = self._file_message_text_view.get_buffer()
597
628
        file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
598
629
        if file_id is None: # Whole tree
599
 
            self._file_message_expander.set_label(_('File commit message'))
 
630
            self._file_message_expander.set_label(_i18n('File commit message'))
600
631
            self._file_message_expander.set_expanded(False)
601
632
            self._file_message_expander.set_sensitive(False)
602
633
            text_buffer.set_text('')
603
634
            self._last_selected_file = None
604
635
        else:
605
 
            self._file_message_expander.set_label(_('Commit message for ')
 
636
            self._file_message_expander.set_label(_i18n('Commit message for ')
606
637
                                                  + display_path)
607
638
            self._file_message_expander.set_expanded(True)
608
639
            self._file_message_expander.set_sensitive(True)
625
656
            if self._commit_all_changes or record[2]:# [2] checkbox
626
657
                file_id = record[0] # [0] file_id
627
658
                path = record[1]    # [1] real path
628
 
                file_message = record[5] # [5] commit message
 
659
                # [5] commit message
 
660
                file_message = _sanitize_and_decode_message(record[5])
629
661
                files.append(path.decode('UTF-8'))
630
662
                if self._enable_per_file_commits and file_message:
631
663
                    # All of this needs to be utf-8 information
 
664
                    file_message = file_message.encode('UTF-8')
632
665
                    file_info.append({'path':path, 'file_id':file_id,
633
666
                                     'message':file_message})
634
667
        file_info.sort(key=lambda x:(x['path'], x['file_id']))
638
671
            return files, []
639
672
 
640
673
    @show_bzr_error
 
674
    def _on_cancel_clicked(self, button):
 
675
        """ Cancel button clicked handler. """
 
676
        self._do_cancel()
 
677
 
 
678
    @show_bzr_error
 
679
    def _on_delete_window(self, source, event):
 
680
        """ Delete window handler. """
 
681
        self._do_cancel()
 
682
 
 
683
    def _do_cancel(self):
 
684
        """If requested, saves commit messages when cancelling gcommit; they are re-used by a next gcommit"""
 
685
        mgr = SavedCommitMessagesManager()
 
686
        self._saved_commit_messages_manager = mgr
 
687
        mgr.insert(self._get_global_commit_message(),
 
688
                   self._get_specific_files()[1])
 
689
        if mgr.is_not_empty(): # maybe worth saving
 
690
            response = self._question_dialog(
 
691
                _i18n('Commit cancelled'),
 
692
                _i18n('Do you want to save your commit messages ?'),
 
693
                parent=self)
 
694
            if response == gtk.RESPONSE_NO:
 
695
                 # save nothing and destroy old comments if any
 
696
                mgr = SavedCommitMessagesManager()
 
697
        mgr.save(self._wt, self._wt.branch)
 
698
        self.response(gtk.RESPONSE_CANCEL) # close window
 
699
 
 
700
    @show_bzr_error
641
701
    def _on_commit_clicked(self, button):
642
702
        """ Commit button clicked handler. """
643
703
        self._do_commit()
647
707
 
648
708
        if message == '':
649
709
            response = self._question_dialog(
650
 
                            _('Commit with an empty message?'),
651
 
                            _('You can describe your commit intent in the message.'))
 
710
                _i18n('Commit with an empty message?'),
 
711
                _i18n('You can describe your commit intent in the message.'),
 
712
                parent=self)
652
713
            if response == gtk.RESPONSE_NO:
653
714
                # Kindly give focus to message area
654
715
                self._global_message_text_view.grab_focus()
667
728
        #       files at this point.
668
729
        for path in self._wt.unknowns():
669
730
            response = self._question_dialog(
670
 
                _("Commit with unknowns?"),
671
 
                _("Unknown files exist in the working tree. Commit anyway?"))
 
731
                _i18n("Commit with unknowns?"),
 
732
                _i18n("Unknown files exist in the working tree. Commit anyway?"),
 
733
                parent=self)
 
734
                # Doesn't set a parent for the dialog..
672
735
            if response == gtk.RESPONSE_NO:
673
736
                return
674
737
            break
686
749
                       revprops=revprops)
687
750
        except errors.PointlessCommit:
688
751
            response = self._question_dialog(
689
 
                                _('Commit with no changes?'),
690
 
                                _('There are no changes in the working tree.'
691
 
                                  ' Do you want to commit anyway?'))
 
752
                _i18n('Commit with no changes?'),
 
753
                _i18n('There are no changes in the working tree.'
 
754
                      ' Do you want to commit anyway?'),
 
755
                parent=self)
692
756
            if response == gtk.RESPONSE_YES:
693
757
                rev_id = self._wt.commit(message,
694
758
                               allow_pointless=True,
697
761
                               specific_files=specific_files,
698
762
                               revprops=revprops)
699
763
        self.committed_revision_id = rev_id
 
764
        # destroy old comments if any
 
765
        SavedCommitMessagesManager().save(self._wt, self._wt.branch)
700
766
        self.response(gtk.RESPONSE_OK)
701
767
 
702
768
    def _get_global_commit_message(self):
703
769
        buf = self._global_message_text_view.get_buffer()
704
770
        start, end = buf.get_bounds()
705
 
        return buf.get_text(start, end).decode('utf-8')
 
771
        text = buf.get_text(start, end)
 
772
        return _sanitize_and_decode_message(text)
706
773
 
707
774
    def _set_global_commit_message(self, message):
708
775
        """Just a helper for the test suite."""
729
796
                                       show_offset=False)
730
797
        rev_dict['revision_id'] = rev.revision_id
731
798
        return rev_dict
 
799
 
 
800
 
 
801
class SavedCommitMessagesManager:
 
802
    """Save glogal and per-file commit messages.
 
803
 
 
804
    Saves global commit message and utf-8 file_id->message dictionary
 
805
    of per-file commit messages on disk. Re-reads them later for re-using.
 
806
    """
 
807
 
 
808
    def __init__(self, tree=None, branch=None):
 
809
        """If branch is None, builds empty messages, otherwise reads them
 
810
        from branch's disk storage. 'tree' argument is for the future."""
 
811
        if branch is None:
 
812
            self.global_message = u''
 
813
            self.file_messages = {}
 
814
        else:
 
815
            config = branch.get_config()._get_branch_data_config()
 
816
            self.global_message = config.get_user_option(
 
817
                'gtk_global_commit_message')
 
818
            if self.global_message is None:
 
819
                self.global_message = u''
 
820
            file_messages = config.get_user_option('gtk_file_commit_messages')
 
821
            if file_messages: # unicode and B-encoded:
 
822
                self.file_messages = bencode.bdecode(
 
823
                    file_messages.encode('UTF-8'))
 
824
            else:
 
825
                self.file_messages = {}
 
826
 
 
827
    def get(self):
 
828
        return self.global_message, self.file_messages
 
829
 
 
830
    def is_not_empty(self):
 
831
        return bool(self.global_message or self.file_messages)
 
832
 
 
833
    def insert(self, global_message, file_info):
 
834
        """Formats per-file commit messages (list of dictionaries, one per file)
 
835
        into one utf-8 file_id->message dictionary and merges this with
 
836
        previously existing dictionary. Merges global commit message too."""
 
837
        file_messages = {}
 
838
        for fi in file_info:
 
839
            file_message = fi['message']
 
840
            if file_message:
 
841
                file_messages[fi['file_id']] = file_message # utf-8 strings
 
842
        for k,v in file_messages.iteritems():
 
843
            try:
 
844
                self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
 
845
            except KeyError:
 
846
                self.file_messages[k] = v
 
847
        if self.global_message:
 
848
            self.global_message = global_message + '\n******\n' \
 
849
                + self.global_message
 
850
        else:
 
851
            self.global_message = global_message
 
852
 
 
853
    def save(self, tree, branch):
 
854
        # We store in branch's config, which can be a problem if two gcommit
 
855
        # are done in two checkouts of one single branch (comments overwrite
 
856
        # each other). Ideally should be in working tree. But uncommit does
 
857
        # not always have a working tree, though it always has a branch.
 
858
        # 'tree' argument is for the future
 
859
        config = branch.get_config()
 
860
        # should it be named "gtk_" or some more neutral name ("gui_" ?) to
 
861
        # be compatible with qbzr in the future?
 
862
        config.set_user_option('gtk_global_commit_message', self.global_message)
 
863
        # bencode() does not know unicode objects but set_user_option()
 
864
        # requires one:
 
865
        config.set_user_option(
 
866
            'gtk_file_commit_messages',
 
867
            bencode.bencode(self.file_messages).decode('UTF-8'))
 
868
 
 
869
 
 
870
def save_commit_messages(local, master, old_revno, old_revid,
 
871
                         new_revno, new_revid):
 
872
    b = local
 
873
    if b is None:
 
874
        b = master
 
875
    mgr = SavedCommitMessagesManager(None, b)
 
876
    revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
 
877
    cur_revno = old_revno
 
878
    new_revision_id = old_revid
 
879
    graph = b.repository.get_graph()
 
880
    for rev_id in revid_iterator:
 
881
        if cur_revno == new_revno:
 
882
            break
 
883
        cur_revno -= 1
 
884
        rev = b.repository.get_revision(rev_id)
 
885
        file_info = rev.properties.get('file-info', None)
 
886
        if file_info is None:
 
887
            file_info = {}
 
888
        else:
 
889
            file_info = bencode.bdecode(file_info.encode('UTF-8'))
 
890
        global_message = osutils.safe_unicode(rev.message)
 
891
        # Concatenate comment of the uncommitted revision
 
892
        mgr.insert(global_message, file_info)
 
893
 
 
894
        parents = graph.get_parent_map([rev_id]).get(rev_id, None)
 
895
        if not parents:
 
896
            continue
 
897
    mgr.save(None, b)