/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: Curtis Hovey
  • Date: 2012-03-11 18:24:06 UTC
  • mto: This revision was merged to the branch mainline in revision 785.
  • Revision ID: sinzui.is@verizon.net-20120311182406-f4jqpff65b30g3al
Create the mnemonic for the button label.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
import re
18
18
 
 
19
from gi.repository import Gdk
19
20
from gi.repository import Gtk
20
21
from gi.repository import GObject
21
22
from gi.repository import Pango
22
23
 
23
24
from bzrlib import (
 
25
    bencode,
24
26
    errors,
25
27
    osutils,
 
28
    revision as _mod_revision,
26
29
    trace,
 
30
    tsort,
27
31
    )
28
 
try:
29
 
    from bzrlib import bencode
30
 
except ImportError:
31
 
    from bzrlib.util import bencode
32
 
 
33
32
from bzrlib.plugins.gtk.dialog import question_dialog
34
33
from bzrlib.plugins.gtk.errors import show_bzr_error
35
34
from bzrlib.plugins.gtk.i18n import _i18n
 
35
from bzrlib.plugins.gtk.commitmsgs import SavedCommitMessagesManager
36
36
 
37
37
try:
38
38
    import dbus
42
42
    have_dbus = False
43
43
 
44
44
 
 
45
def _get_sorted_revisions(tip_revision, revision_ids, parent_map):
 
46
    """Get an iterator which will return the revisions in merge sorted order.
 
47
 
 
48
    This will build up a list of all nodes, such that only nodes in the list
 
49
    are referenced. It then uses MergeSorter to return them in 'merge-sorted'
 
50
    order.
 
51
 
 
52
    :param revision_ids: A set of revision_ids
 
53
    :param parent_map: The parent information for each node. Revisions which
 
54
        are considered ghosts should not be present in the map.
 
55
    :return: iterator from MergeSorter.iter_topo_order()
 
56
    """
 
57
    # MergeSorter requires that all nodes be present in the graph, so get rid
 
58
    # of any references pointing outside of this graph.
 
59
    parent_graph = {}
 
60
    for revision_id in revision_ids:
 
61
        if revision_id not in parent_map: # ghost
 
62
            parent_graph[revision_id] = []
 
63
        else:
 
64
            # Only include parents which are in this sub-graph
 
65
            parent_graph[revision_id] = [p for p in parent_map[revision_id]
 
66
                                            if p in revision_ids]
 
67
    sorter = tsort.MergeSorter(parent_graph, tip_revision)
 
68
    return sorter.iter_topo_order()
 
69
 
 
70
 
45
71
def pending_revisions(wt):
46
72
    """Return a list of pending merges or None if there are none of them.
47
73
 
52
78
    """
53
79
    parents = wt.get_parent_ids()
54
80
    if len(parents) < 2:
55
 
        return None
 
81
        return
56
82
 
57
83
    # The basic pending merge algorithm uses the same algorithm as
58
84
    # bzrlib.status.show_pending_merges
60
86
    branch = wt.branch
61
87
    last_revision = parents[0]
62
88
 
63
 
    if last_revision is not None:
64
 
        try:
65
 
            ignore = set(branch.repository.get_ancestry(last_revision,
66
 
                                                        topo_sorted=False))
67
 
        except errors.NoSuchRevision:
68
 
            # the last revision is a ghost : assume everything is new
69
 
            # except for it
70
 
            ignore = set([None, last_revision])
71
 
    else:
72
 
        ignore = set([None])
 
89
    graph = branch.repository.get_graph()
 
90
    other_revisions = [last_revision]
73
91
 
74
92
    pm = []
75
93
    for merge in pending:
76
 
        ignore.add(merge)
77
 
        try:
78
 
            rev = branch.repository.get_revision(merge)
79
 
            children = []
80
 
            pm.append((rev, children))
81
 
 
82
 
            # This does need to be topo sorted, so we search backwards
83
 
            inner_merges = branch.repository.get_ancestry(merge)
84
 
            assert inner_merges[0] is None
85
 
            inner_merges.pop(0)
86
 
            for mmerge in reversed(inner_merges):
87
 
                if mmerge in ignore:
88
 
                    continue
89
 
                rev = branch.repository.get_revision(mmerge)
90
 
                children.append(rev)
91
 
 
92
 
                ignore.add(mmerge)
93
 
        except errors.NoSuchRevision:
94
 
            print "DEBUG: NoSuchRevision:", merge
95
 
 
96
 
    return pm
 
94
        try:
 
95
            merge_rev = branch.repository.get_revision(merge)
 
96
        except errors.NoSuchRevision:
 
97
            # If we are missing a revision, just print out the revision id
 
98
            trace.mutter("ghost: %r", merge)
 
99
            other_revisions.append(merge)
 
100
            continue
 
101
 
 
102
        # Find all of the revisions in the merge source, which are not in the
 
103
        # last committed revision.
 
104
        merge_extra = graph.find_unique_ancestors(merge, other_revisions)
 
105
        other_revisions.append(merge)
 
106
        merge_extra.discard(_mod_revision.NULL_REVISION)
 
107
 
 
108
        # Get a handle to all of the revisions we will need
 
109
        try:
 
110
            revisions = dict((rev.revision_id, rev) for rev in
 
111
                             branch.repository.get_revisions(merge_extra))
 
112
        except errors.NoSuchRevision:
 
113
            # One of the sub nodes is a ghost, check each one
 
114
            revisions = {}
 
115
            for revision_id in merge_extra:
 
116
                try:
 
117
                    rev = branch.repository.get_revisions([revision_id])[0]
 
118
                except errors.NoSuchRevision:
 
119
                    revisions[revision_id] = None
 
120
                else:
 
121
                    revisions[revision_id] = rev
 
122
 
 
123
         # Display the revisions brought in by this merge.
 
124
        rev_id_iterator = _get_sorted_revisions(merge, merge_extra,
 
125
                            branch.repository.get_parent_map(merge_extra))
 
126
        # Skip the first node
 
127
        num, first, depth, eom = rev_id_iterator.next()
 
128
        if first != merge:
 
129
            raise AssertionError('Somehow we misunderstood how'
 
130
                ' iter_topo_order works %s != %s' % (first, merge))
 
131
        children = []
 
132
        for num, sub_merge, depth, eom in rev_id_iterator:
 
133
            rev = revisions[sub_merge]
 
134
            if rev is None:
 
135
                trace.warning("ghost: %r", sub_merge)
 
136
                continue
 
137
            children.append(rev)
 
138
        yield (merge_rev, children)
97
139
 
98
140
 
99
141
_newline_variants_re = re.compile(r'\r\n?')
100
142
def _sanitize_and_decode_message(utf8_message):
101
143
    """Turn a utf-8 message into a sanitized Unicode message."""
102
144
    fixed_newline = _newline_variants_re.sub('\n', utf8_message)
103
 
    return fixed_newline.decode('utf-8')
 
145
    return osutils.safe_unicode(fixed_newline)
104
146
 
105
147
 
106
148
class CommitDialog(Gtk.Dialog):
107
149
    """Implementation of Commit."""
108
150
 
109
151
    def __init__(self, wt, selected=None, parent=None):
110
 
        GObject.GObject.__init__(self, title="Commit to %s" % wt.basedir,
111
 
                            parent=parent, flags=0,)
 
152
        super(CommitDialog, self).__init__(
 
153
            title="Commit to %s" % wt.basedir, parent=parent, flags=0)
112
154
        self.connect('delete-event', self._on_delete_window)
113
155
        self._question_dialog = question_dialog
114
156
 
121
163
        self._enable_per_file_commits = True
122
164
        self._commit_all_changes = True
123
165
        self.committed_revision_id = None # Nothing has been committed yet
124
 
        self._saved_commit_messages_manager = SavedCommitMessagesManager(self._wt, self._wt.branch)
 
166
        self._last_selected_file = None
 
167
        self._saved_commit_messages_manager = SavedCommitMessagesManager(
 
168
            self._wt, self._wt.branch)
125
169
 
126
170
        self.setup_params()
127
171
        self.construct()
131
175
        """Setup the member variables for state."""
132
176
        self._basis_tree = self._wt.basis_tree()
133
177
        self._delta = None
134
 
        self._pending = pending_revisions(self._wt)
 
178
        self._wt.lock_read()
 
179
        try:
 
180
            self._pending = list(pending_revisions(self._wt))
 
181
        finally:
 
182
            self._wt.unlock()
135
183
 
136
184
        self._is_checkout = (self._wt.branch.get_bound_location() is not None)
137
185
 
189
237
 
190
238
        all_enabled = (self._selected is None)
191
239
        # The first entry is always the 'whole tree'
192
 
        all_iter = store.append([None, None, all_enabled, 'All Files', '', ''])
 
240
        all_iter = store.append(["", "", all_enabled, 'All Files', '', ''])
193
241
        initial_cursor = store.get_path(all_iter)
194
242
        # should we pass specific_files?
195
243
        self._wt.lock_read()
226
274
        # This sets the cursor, which causes the expander to close, which
227
275
        # causes the _file_message_text_view to never get realized. So we have
228
276
        # to give it a little kick, or it warns when we try to grab the focus
229
 
        self._treeview_files.set_cursor(initial_cursor)
 
277
        self._treeview_files.set_cursor(initial_cursor, None, False)
230
278
 
231
279
        def _realize_file_message_tree_view(*args):
232
280
            self._file_message_text_view.realize()
240
288
            self._check_local.hide()
241
289
            return
242
290
        if have_dbus:
243
 
            bus = dbus.SystemBus()
 
291
            try:
 
292
                bus = dbus.SystemBus()
 
293
            except dbus.DBusException:
 
294
                trace.mutter("DBus system bus not available")
 
295
                self._check_local.show()
 
296
                return
244
297
            try:
245
298
                proxy_obj = bus.get_object('org.freedesktop.NetworkManager',
246
299
                                           '/org/freedesktop/NetworkManager')
248
301
                trace.mutter("networkmanager not available.")
249
302
                self._check_local.show()
250
303
                return
251
 
            
 
304
 
252
305
            dbus_iface = dbus.Interface(proxy_obj,
253
306
                                        'org.freedesktop.NetworkManager')
254
307
            try:
280
333
        """Build up the dialog widgets."""
281
334
        # The primary pane which splits it into left and right (adjustable)
282
335
        # sections.
283
 
        self._hpane = Gtk.HPaned()
 
336
        self._hpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
284
337
 
285
338
        self._construct_left_pane()
286
339
        self._construct_right_pane()
287
340
        self._construct_action_pane()
288
341
 
289
 
        self.vbox.pack_start(self._hpane, True, True, 0)
 
342
        self.get_content_area().pack_start(self._hpane, True, True, 0)
290
343
        self._hpane.show()
291
344
        self.set_focus(self._global_message_text_view)
292
345
 
312
365
 
313
366
    def _construct_accelerators(self):
314
367
        group = Gtk.AccelGroup()
315
 
        group.connect_group(Gdk.keyval_from_name('N'),
316
 
                            Gdk.EventMask.CONTROL_MASK, 0, self._on_accel_next)
 
368
        group.connect(Gdk.keyval_from_name('N'),
 
369
                      Gdk.ModifierType.CONTROL_MASK, 0, self._on_accel_next)
317
370
        self.add_accel_group(group)
318
371
 
319
372
        # ignore the escape key (avoid closing the window)
326
379
 
327
380
        self._check_local = Gtk.CheckButton(_i18n("_Only commit locally"),
328
381
                                            use_underline=True)
329
 
        self._left_pane_box.pack_end(self._check_local, False, False)
 
382
        self._left_pane_box.pack_end(self._check_local, False, False, 0)
330
383
        self._check_local.set_active(False)
331
384
 
332
385
        self._hpane.pack1(self._left_pane_box, resize=False, shrink=False)
354
407
        self._button_cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
355
408
        self._button_cancel.connect('clicked', self._on_cancel_clicked)
356
409
        self._button_cancel.show()
357
 
        self.action_area.pack_end(self._button_cancel)
 
410
        self.get_action_area().pack_end(
 
411
            self._button_cancel, True, True, 0)
358
412
        self._button_commit = Gtk.Button(_i18n("Comm_it"), use_underline=True)
359
413
        self._button_commit.connect('clicked', self._on_commit_clicked)
360
414
        self._button_commit.set_can_default(True)
361
415
        self._button_commit.show()
362
 
        self.action_area.pack_end(self._button_commit)
 
416
        self.get_action_area().pack_end(
 
417
            self._button_commit, True, True, 0)
363
418
        self._button_commit.grab_default()
364
419
 
365
420
    def _add_to_right_table(self, widget, weight, expanding=False):
385
440
        # file_label.show()
386
441
        self._files_box.pack_start(file_label, False, True, 0)
387
442
 
388
 
        self._commit_all_files_radio = Gtk.RadioButton(
 
443
        self._commit_all_files_radio = Gtk.RadioButton.new_with_label(
389
444
            None, _i18n("Commit all changes"))
390
445
        self._files_box.pack_start(self._commit_all_files_radio, False, True, 0)
391
446
        self._commit_all_files_radio.show()
392
447
        self._commit_all_files_radio.connect('toggled',
393
448
            self._toggle_commit_selection)
394
 
        self._commit_selected_radio = Gtk.RadioButton(
 
449
        self._commit_selected_radio = Gtk.RadioButton.new_with_label_from_widget(
395
450
            self._commit_all_files_radio, _i18n("Only commit selected changes"))
396
451
        self._files_box.pack_start(self._commit_selected_radio, False, True, 0)
397
452
        self._commit_selected_radio.show()
409
464
        scroller.add(self._treeview_files)
410
465
        scroller.set_shadow_type(Gtk.ShadowType.IN)
411
466
        scroller.show()
412
 
        self._files_box.pack_start(scroller,
413
 
                                   expand=True, fill=True)
 
467
        self._files_box.pack_start(scroller, True, True, 0)
414
468
        self._files_box.show()
415
469
        self._left_pane_box.pack_start(self._files_box, True, True, 0)
416
470
 
445
499
                                     self._on_treeview_files_cursor_changed)
446
500
 
447
501
    def _toggle_commit(self, cell, path, model):
448
 
        if model[path][0] is None: # No file_id means 'All Files'
 
502
        if model[path][0] == "": # No file_id means 'All Files'
449
503
            new_val = not model[path][2]
450
504
            for node in model:
451
505
                node[2] = new_val
461
515
                checked_col.set_visible(False)
462
516
            else:
463
517
                checked_col.set_visible(True)
464
 
            renderer = checked_col.get_cell_renderers()[0]
 
518
            renderer = checked_col.get_cells()[0]
465
519
            renderer.set_property('activatable', not all_files)
466
520
 
467
521
    def _construct_pending_list(self):
473
527
        pending_message = Gtk.Label()
474
528
        pending_message.set_markup(
475
529
            _i18n('<i>* Cannot select specific files when merging</i>'))
476
 
        self._pending_box.pack_start(pending_message, expand=False, padding=5)
 
530
        self._pending_box.pack_start(pending_message, False, True, 5)
477
531
        pending_message.show()
478
532
 
479
533
        pending_label = Gtk.Label(label=_i18n('Pending Revisions'))
480
 
        self._pending_box.pack_start(pending_label, expand=False, padding=0)
 
534
        self._pending_box.pack_start(pending_label, False, True, 0)
481
535
        pending_label.show()
482
536
 
483
537
        scroller = Gtk.ScrolledWindow()
486
540
        scroller.add(self._treeview_pending)
487
541
        scroller.set_shadow_type(Gtk.ShadowType.IN)
488
542
        scroller.show()
489
 
        self._pending_box.pack_start(scroller,
490
 
                                     expand=True, fill=True, padding=5)
 
543
        self._pending_box.pack_start(scroller, True, True, 5)
491
544
        self._treeview_pending.show()
492
545
        self._left_pane_box.pack_start(self._pending_box, True, True, 0)
493
546
 
536
589
        self._file_message_text_view.set_accepts_tab(False)
537
590
        self._file_message_text_view.show()
538
591
 
539
 
        self._file_message_expander = Gtk.Expander(_i18n('File commit message'))
 
592
        self._file_message_expander = Gtk.Expander(
 
593
            label=_i18n('File commit message'))
540
594
        self._file_message_expander.set_expanded(True)
541
595
        self._file_message_expander.add(scroller)
542
596
        self._add_to_right_table(self._file_message_expander, 1, False)
568
622
 
569
623
    def _on_treeview_files_cursor_changed(self, treeview):
570
624
        treeselection = treeview.get_selection()
 
625
        if treeselection is None:
 
626
            # The treeview was probably destroyed as the dialog closes.
 
627
            return
571
628
        (model, selection) = treeselection.get_selected()
572
629
 
573
630
        if selection is not None:
574
631
            path, display_path = model.get(selection, 1, 3)
575
632
            self._diff_label.set_text(_i18n('Diff for ') + display_path)
576
 
            if path is None:
 
633
            if path == "":
577
634
                self._diff_view.show_diff(None)
578
635
            else:
579
 
                self._diff_view.show_diff([path.decode('UTF-8')])
 
636
                self._diff_view.show_diff([osutils.safe_unicode(path)])
580
637
            self._update_per_file_info(selection)
581
638
 
582
639
    def _on_accel_next(self, accel_group, window, keyval, modifier):
593
650
            # We have either made it to the end of the list, or nothing was
594
651
            # selected. Either way, select All Files, and jump to the global
595
652
            # commit message.
596
 
            self._treeview_files.set_cursor((0,))
 
653
            self._treeview_files.set_cursor(
 
654
                Gtk.TreePath(path=0), "", False)
597
655
            self._global_message_text_view.grab_focus()
598
656
        else:
599
657
            # Set the cursor to this entry, and jump to the per-file commit
600
658
            # message
601
 
            self._treeview_files.set_cursor(model.get_path(next))
 
659
            self._treeview_files.set_cursor(model.get_path(next), None, False)
602
660
            self._file_message_text_view.grab_focus()
603
661
 
604
662
    def _save_current_file_message(self):
606
664
            return # Nothing to save
607
665
        text_buffer = self._file_message_text_view.get_buffer()
608
666
        cur_text = text_buffer.get_text(text_buffer.get_start_iter(),
609
 
                                        text_buffer.get_end_iter())
 
667
                                        text_buffer.get_end_iter(), True)
610
668
        last_selected = self._files_store.get_iter(self._last_selected_file)
611
669
        self._files_store.set_value(last_selected, 5, cur_text)
612
670
 
618
676
        self._save_current_file_message()
619
677
        text_buffer = self._file_message_text_view.get_buffer()
620
678
        file_id, display_path, message = self._files_store.get(selection, 0, 3, 5)
621
 
        if file_id is None: # Whole tree
 
679
        if file_id == "": # Whole tree
622
680
            self._file_message_expander.set_label(_i18n('File commit message'))
623
681
            self._file_message_expander.set_expanded(False)
624
682
            self._file_message_expander.set_sensitive(False)
641
699
        files = []
642
700
        records = iter(self._files_store)
643
701
        rec = records.next() # Skip the All Files record
644
 
        assert rec[0] is None, "Are we skipping the wrong record?"
 
702
        assert rec[0] == "", "Are we skipping the wrong record?"
645
703
 
646
704
        file_info = []
647
705
        for record in records:
648
706
            if self._commit_all_changes or record[2]:# [2] checkbox
649
 
                file_id = record[0] # [0] file_id
650
 
                path = record[1]    # [1] real path
 
707
                file_id = osutils.safe_utf8(record[0]) # [0] file_id
 
708
                path = osutils.safe_utf8(record[1])    # [1] real path
651
709
                # [5] commit message
652
710
                file_message = _sanitize_and_decode_message(record[5])
653
711
                files.append(path.decode('UTF-8'))
760
818
    def _get_global_commit_message(self):
761
819
        buf = self._global_message_text_view.get_buffer()
762
820
        start, end = buf.get_bounds()
763
 
        text = buf.get_text(start, end)
 
821
        text = buf.get_text(start, end, True)
764
822
        return _sanitize_and_decode_message(text)
765
823
 
766
824
    def _set_global_commit_message(self, message):
789
847
        rev_dict['revision_id'] = rev.revision_id
790
848
        return rev_dict
791
849
 
792
 
 
793
 
class SavedCommitMessagesManager:
794
 
    """Save glogal and per-file commit messages.
795
 
 
796
 
    Saves global commit message and utf-8 file_id->message dictionary
797
 
    of per-file commit messages on disk. Re-reads them later for re-using.
798
 
    """
799
 
 
800
 
    def __init__(self, tree=None, branch=None):
801
 
        """If branch is None, builds empty messages, otherwise reads them
802
 
        from branch's disk storage. 'tree' argument is for the future."""
803
 
        if branch is None:
804
 
            self.global_message = u''
805
 
            self.file_messages = {}
806
 
        else:
807
 
            config = branch.get_config()
808
 
            self.global_message = config.get_user_option(
809
 
                '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(
815
 
                    file_messages.encode('UTF-8'))
816
 
            else:
817
 
                self.file_messages = {}
818
 
 
819
 
    def get(self):
820
 
        return self.global_message, self.file_messages
821
 
 
822
 
    def is_not_empty(self):
823
 
        return bool(self.global_message or self.file_messages)
824
 
 
825
 
    def insert(self, global_message, file_info):
826
 
        """Formats per-file commit messages (list of dictionaries, one per file)
827
 
        into one utf-8 file_id->message dictionary and merges this with
828
 
        previously existing dictionary. Merges global commit message too."""
829
 
        file_messages = {}
830
 
        for fi in file_info:
831
 
            file_message = fi['message']
832
 
            if file_message:
833
 
                file_messages[fi['file_id']] = file_message # utf-8 strings
834
 
        for k,v in file_messages.iteritems():
835
 
            try:
836
 
                self.file_messages[k] = v + '\n******\n' + self.file_messages[k]
837
 
            except KeyError:
838
 
                self.file_messages[k] = v
839
 
        if self.global_message:
840
 
            self.global_message = global_message + '\n******\n' \
841
 
                + self.global_message
842
 
        else:
843
 
            self.global_message = global_message
844
 
 
845
 
    def save(self, tree, branch):
846
 
        # We store in branch's config, which can be a problem if two gcommit
847
 
        # are done in two checkouts of one single branch (comments overwrite
848
 
        # each other). Ideally should be in working tree. But uncommit does
849
 
        # not always have a working tree, though it always has a branch.
850
 
        # 'tree' argument is for the future
851
 
        config = branch.get_config()
852
 
        # should it be named "gtk_" or some more neutral name ("gui_" ?) to
853
 
        # be compatible with qbzr in the future?
854
 
        config.set_user_option('gtk_global_commit_message', self.global_message)
855
 
        # bencode() does not know unicode objects but set_user_option()
856
 
        # requires one:
857
 
        config.set_user_option(
858
 
            'gtk_file_commit_messages',
859
 
            bencode.bencode(self.file_messages).decode('UTF-8'))
860
 
 
861
 
 
862
 
def save_commit_messages(local, master, old_revno, old_revid,
863
 
                         new_revno, new_revid):
864
 
    b = local
865
 
    if b is None:
866
 
        b = master
867
 
    mgr = SavedCommitMessagesManager(None, b)
868
 
    revid_iterator = b.repository.iter_reverse_revision_history(old_revid)
869
 
    cur_revno = old_revno
870
 
    new_revision_id = old_revid
871
 
    graph = b.repository.get_graph()
872
 
    for rev_id in revid_iterator:
873
 
        if cur_revno == new_revno:
874
 
            break
875
 
        cur_revno -= 1
876
 
        rev = b.repository.get_revision(rev_id)
877
 
        file_info = rev.properties.get('file-info', None)
878
 
        if file_info is None:
879
 
            file_info = {}
880
 
        else:
881
 
            file_info = bencode.bdecode(file_info.encode('UTF-8'))
882
 
        global_message = osutils.safe_unicode(rev.message)
883
 
        # Concatenate comment of the uncommitted revision
884
 
        mgr.insert(global_message, file_info)
885
 
 
886
 
        parents = graph.get_parent_map([rev_id]).get(rev_id, None)
887
 
        if not parents:
888
 
            continue
889
 
    mgr.save(None, b)